// 3/7/12 4/12/12 1/21/13 2/10/15 // The constants can be modified as needed. int theWidth = 500; // Width of canvas int theHeight = 360; // Height of canvas // It is assumed that both .ogg and .mp4 versions of the files are available // in the same folder. The file names must be complete URLs except for the // ending .ogg or .mp4 which must be omitted. It will be added as needed for // the browser. Eception: .mp4 and webm versions of jwplayerFeatures are // available. Use the prefix "file:///" or "http:// as appropriate. String[] files = {"vid/developerStories-en", "vid/Kitten", "vid/jwplayerFeatures"}; Video video = document.getElementById("videotag"); String videoStatus, eventStatus; CircleButton playBtn, pauseBtn, stopBtn; CircleButton volumeDownBtn, volumeUpBtn, timeDownBtn, timeUpBtn; ToggleButton[] fileBtn = new ToggleButton[files.length]; // -------------------- Global constants for tracks -------------------- final String NEW_LINE = "
"; final String INDENT_NEW_LINE = "
    "; final int UNKNOWN = -1; // current track number source has not been determined final int USECODE = -2; // the track will be generated by Processing.js code final int USEHTML = -3; // the track will be created from info in HTML file final int OFFSET_TRACKS = 100; // Added to a track number that has // *********** Global fields for tracks *********** int videoNo = 0; // current video number, initialized to the first video int languageNo; // current languge number int trackNo; // current track number HTMLTrackElement htmlTrack; // an HTML TrackElement object Track aTrack; // the current track String trackId; // current track identifier TextTrackCue[] cues; // the list of cues for the current track TextTrackCue cue; // the current cue boolean showTheInfo; // if true, show the extra information boolean readVTT; // if true, use XMLHttpRequest to read .vtt file in the future boolean useVTTCue = true; // Can browser use VTTCue (IE 11 can't) Track[] arrayTracks = null; // An array of available tracks. Set up first use String[][] trackLangs = {{"English", "Spanish", "German"}, // video number 0 {"English", "German"}, // video number 1 {"English", "French"}}; // video number 2 String[][] trackIds = {{"enTrack0", "spTrack0", "deTrack0"}, // video number 0 {"enTrack1", "deTrack1"}, // video number 1 {"enTrack2", "frTrack2"}}; // video number 2 int[][] useTrack = {{OFFSET_TRACKS+0, OFFSET_TRACKS+1, USECODE},// for video 0 {USEHTML, OFFSET_TRACKS+2}, // for video number 1 {OFFSET_TRACKS+3, OFFSET_TRACKS+4}}; // for video number 2 // The numbers OFFSET_TRACKS+? indicated that a tag was // created for that language. They are numbered in order of appearance. // USECODE means the cues will be generated with Processing.js code. // USEHTML means that the cues will always be read from the HTML file. String[][] trackSource = {{UNKNOWN, UNKNOWN, UNKNOWN}, // for video number 0 {UNKNOWN, UNKNOWN}, // for video number 1 {UNKNOWN, UNKNOWN}}; // for video number 2 // replace "UNKNOWN" by file URL, "HTML file", or "Generated by code" int numTracks = trackLangs[videoNo].length; // number of tracks for the current video ToggleButton[] trackBtn = new ToggleButton[numTracks]; // buttons for identifiers ToggleButton hideSubtitlesBtn; // Button for hiding subtitles ToggleButton showInfoBtn; // Button for showing the extra info ToggleButton readVTTBtn; // Button for saying that XMLHttpRequest should be used String data; String msgStr, cueStr; void setup() { // Setup the sketch size(theWidth, theHeight); int x, y, i; smooth(); frameRate(20); // add events video.addEventListener("loadstart", loadStart, false); video.addEventListener("playing", playing, false); video.addEventListener("pause", pause, false); video.addEventListener("ended", ended, false); video.addEventListener("error", error, false); video.addEventListener("waiting", waiting, false); video.addEventListener("suspend", suspend, false); // construct buttons playBtn = new CircleButton(130, 165,"Play"); pauseBtn = new CircleButton(230, 165, "Pause"); stopBtn = new CircleButton(330, 165, "Stop"); volumeDownBtn = new CircleButton(165, 190, "Volume down"); volumeUpBtn = new CircleButton(270, 190, "Volume up"); timeDownBtn = new CircleButton(165, 215, "Time down"); timeUpBtn = new CircleButton(270, 215, "Time up"); y = 280; for (i = 0; i < files.length; i++) { fileBtn[i] = new ToggleButton(20, y, files[i], false); y += 25; } fileBtn[videoNo].setState(true); // ************************* Track code ************************ x = 60; for (i = 0; i < numTracks; i++) { trackBtn[i] = new ToggleButton(x, 245, trackLangs[videoNo][i], false); x += 105; } // Next 3 buttons: set true or false for desired initial condition hideSubtitlesBtn = new ToggleButton(400, 300, "Hide subtitles", false); showInfoBtn = new ToggleButton(382, 325, "Show infomation", false); readVTTBtn = new ToggleButton(395, 350, "Read VTT files", false); // specify initial track languageNo = 0; //Current track languge trackBtn[languageNo].setState(true); // set track button // additional initializing videoStatus = ""; eventStatus = ""; setSource(files[videoNo]); // pick the initial video file showTheInfo = showInfoBtn.getState(); readVTT = readVTTBtn.getState(); // initialize tracks but wait 2 seconds setTimeout(initializeTracks(), 2000); } // setup void draw() { // Draws the sketch on the canvas int i; try { //primarily for debugging background(#FFFFAA); fill(#000000); textAlign(CENTER); text("KISVid T", width/2, 30); text("Source file: " + video.src, width/2, 60); text("Track source: " + trackSource[videoNo][languageNo], width/2, 80); text("Status: " + videoStatus, width/2, 100); text("Current time: " + round(video.currentTime) + " sec. Length: " + round(video.duration)+ " sec.", width/2, 120); text("Volume (0 to 1): " + round(10 * video.volume)/10.0, width/2, 140); playBtn.draw(); pauseBtn.draw(); stopBtn.draw(); volumeDownBtn.draw(); volumeUpBtn.draw(); timeDownBtn.draw(); timeUpBtn.draw(); for (i = 0; i < files.length; i++) { fileBtn[i].draw(); } showInfoBtn.draw(); readVTTBtn.draw(); hideSubtitlesBtn.draw(); for (i = 0; i < useTrack[videoNo].length; i++) trackBtn[i].draw(); if (eventStatus == "waiting") fill(#FF0000); text("Event status:" + eventStatus, 10, 355); } catch (Exception e) { text("error", 20, 20); } } // draw void setSource(String url ) { // Called to set the source file. Do not include the file extension // in the url. It will be added here. try { if (videoNo != 2 && video.canPlayType && video.canPlayType("video/ogg")) { video.setAttribute("src", url + ".ogg"); } else if (videoNo == 2 && video.canPlayType && video.canPlayType("video/webm")) { video.setAttribute("src", url + ".webm"); } else { video.setAttribute("src", url + ".mp4"); } } catch (Exception e) { video.setAttribute("src", url + ".mp4"); } videoStatus = "File selected"; } // setSource void stop() { // Called stop playing the file by pausing it and setting the // time back to 0; video.pause(); video.currentTime = 0; videoStatus = "Stopped"; eventStatus = "stopped"; } // stop void loadStart() { // LoadStart event processing videoStatus = "Loading"; eventStatus = "loading"; } // loadStart void playing() { // Playing event processing videoStatus = "Playing"; eventStatus = "playing"; } // playing void pause() { // Pause event processing. // There is no stop event but a "stop" causes a pause. This // method checks to see if the current time = 0. If so it assumes // stopped if (video.currentTime == 0) videoStatus = "Stopped"; else videoStatus = "Paused"; eventStatus = "paused"; } // pause void ended() { // Ending event processing videoStatus = "Finished playing"; eventStatus = "ended"; } //ended void error() { // error event processing videoStatus = "Error"; eventStatus = "error"; } //error void waiting() { eventStatus = "waiting"; } // waiting void suspend() { eventStatus = "suspend"; } // suspend void mouseClicked() { // Mouse clicked event processing var v, t; int i; if (playBtn.isOver()) { video.play(); } else if (pauseBtn.isOver()) { video.pause(); } else if (stopBtn.isOver()) { stop(); } else if (volumeUpBtn.isOver()) { v = video.volume + 0.1; video.volume = constrain(v, 0, 1); } else if (volumeDownBtn.isOver()) { v = video.volume - 0.1; video.volume = constrain(v, 0, 1); } else if (timeUpBtn.isOver()) { t = video.currentTime + 0.1 * video.duration; video.currentTime = constrain(t, 0, video.duration); } else if (timeDownBtn.isOver()) { t = video.currentTime - 0.1 * video.duration; video.currentTime = constrain(t, 0, video.duration); } else if (hideSubtitlesBtn.isOver()) { hideSubtitlesBtn.toggleState(); hideSubtitles(); } else if (showInfoBtn.isOver()) { showInfoBtn.toggleState(); showTheInfo = showInfoBtn.getState(); showInfo(""); } else if (readVTTBtn.isOver()) { readVTTBtn.toggleState(); readVTT = readVTTBtn.getState(); showInfo("mouseClicked"); // displaySources(); } else { for (i = 0; i < fileBtn.length; i++) { if (fileBtn[i].isOver()) { videoNo = i; setSource(files[i]); setRadioButton(fileBtn,i); setupTrack(0); return; } } for (i = 0; i < useTrack[videoNo].length; i++) { if (trackBtn[i].isOver()) { setupTrack(i); return; } } } } // mouseClicked // *********************** Track code *********************** /** * common code excuted when either the video or track is changed */ void setupTrack(int aTrackNo) { aTrack.mode = "hidden"; // hide the current track languageNo = aTrackNo; setRadioButton(trackBtn, aTrackNo); initializeTracks(); } // setupTrack /** * initualize the track give the video number, track * number and track id. */ void initializeTracks(){ int i; String s; char c; int useTrackCode; String sourceFile; try { msgStr = " ***VideoNo: " + videoNo; // first time initializing - VTTCue and arrayTracks if (arrayTracks == null) { // first time initArrayTracks(); for (i = 0; i < arrayTracks.length; i++) { arrayTracks[i].mode = "hidden"; } } else { msgArrayTracks(); } // Set the trackID and get infomation about the track trackId = trackIds[videoNo][languageNo]; getTrackInfo(); // Use an appropriate method to establish cues useTrackCode = useTrack[videoNo][languageNo]; if (useTrackCode >= 0 && useTrackCode < OFFSET_TRACKS) { // The track is already set up and ready to be played KISVidTBookKeeping(useTrackCode, trackSource[videoNo][languageNo]); } else if (useTrackCode == USECODE) { // generate cues with Processing.js code generateCodeCues(trackLangs[videoNo][languageNo], trackId); } else if (useTrackCode == USEHTML) { // create cues by reading them form the .html file readCuesFromHTML(trackLangs[videoNo][languageNo], trackId, "EnglishData1"); } else { // the tag should exist trackNo = useTrackCode - OFFSET_TRACKS; try { // required because cues == null if arrayTracks[trackNo].mode = "hidden"; } catch (Exception e) { } if (arrayTracks[trackNo].cues != null && arrayTracks[trackNo].cues.length > 0 && !readVTT) { // the cues have been read from a .vtt file useStandardMethod(); } else { // the exists but will not be read in the normal // fashion. Use XMLHttpRequest() to read file. msgStr += NEW_LINE + "***Read cues from VTT file"; trackInHTML = document.getElementById(trackId); sourceFile = trackInHTML.src; trackSource[videoNo][languageNo] = sourceFile; readCuesFromVttFile(trackLangs[videoNo][languageNo], trackId, sourceFile); } } } catch (Exception e) { msgStr += NEW_LINE + "***InitializeTrack error: " + e + "
***trackNo: " + trackNo + ", trackId: " + trackId + "
"; alert("Error in initializeTracks: " + e); } for (i = 0; i < useTrack[videoNo].length; i++) { trackBtn[i].setLabel(useTrack[videoNo][i] + " " + trackLangs[videoNo][i]); } hideSubtitlesBtn.setState(false); // set true or false as desired msgArrayTracksList(); showInfo("initializeTracks"); //displaySources(); showInfo("Tracks initialized"); video.play(); } // initializeTracks void initArrayTracks() { // determine if VTTCue is defined (it isn't in IE 11) determineVTTCue(); msgStr += INDENT_NEW_LINE + "useVTTCue: " + useVTTCue; // assign arrayTracks arrayTracks = video.textTracks; msgArrayTracks(); } // initArrayTracks /** * get information about the appropriate tag */ void getTrackInfo() { try { htmlTrack = document.getElementById(trackId); } catch (Exception e) { msgStr += NEW_LINE + "***There is no tag for this track" + "
trackId: " + trackId; } showInfo("getTrackInfo"); } // getTrackInfo /** * This method is called when switching to a previously used track */ void useExistingTrack() { aTrack = arrayTracks[useTrack[videoNo][languageNo]]; aTrack.mode = "showing"; msgUseExistingTrack(); cues = aTrack.cues; KISVidTBookKeeping(useTrack[videoNo][languageNo], trackSource[videoNo][languageNo]); } // useExistingTrack /** * Use this methods when it appears that we can uses cues in a .vtt file */ void useStandardMethod() { try { KISVidTBookKeeping(useTrack[videoNo][languageNo] - OFFSET_TRACKS, htmlTrack.src); msgStr += NEW_LINE + "***Using cues in a VTT file"; } catch (Exception e) { msgStr += NEW_LINE + "*** useStandardMethod error: " + e +"
trackNo: " + trackNo; } } // useStandardMethod /** * This methods is called to modify the cues in a track */ void modifyCues() { if (videoNo == 0 && languageNo == 1 && cues.length == 22) { // Developer stories/Spanish // Originally there were were 22 cues in the Spanish track. // After modification there will be 21 or 23 modifySpanish(); showInfo("modifyCues"); } else if (videoNo == 2 && languageNo == 0) { // attempt to get jwplayersFeatures video to pause at end of cue 14. // does not work in Firefox 34 aTrack = arrayTracks[trackNo]; msgStr += NEW_LINE + "kkkk aTrack.length: " + aTrack.cues.length + " trackNo: " + trackNo; aTrack.cues[14].pauseOnExit = true; msgStr += NEW_LINE + "***Marked cue for pause on exit: " + aTrack.cues[14].pauseOnExit; } } // modifyCues /** * It is possible to add cues to a track but the method depends on * the browser. Firefox 35 creates new cues with VTTCue but * IE 11 does not recognize VTTCue. This routine determines if * VTTCue can be used. */ void determineVTTCue() { try { if (VTTCue) // This may cause an error (e.g. IE 11) useVTTCue = true; else useVTTCue = false; } catch (Exception err) { useVTTCue = false; } } // determineVTTCue /** * Toggle the hidden/showing mode */ void hideSubtitles() { if (aTrack.mode == "hidden") { aTrack.mode = "showing"; } else { aTrack.mode = "hidden"; } } // showSubtitles /** * Creates a new cue. Two different methods are needed because IE 11 * does not support VTTCue and FireFox 34, Opera 26, and Chrome 39 not * support TextTrackCue. This method picks one that will work based on * "useVTTCue" as was determined in initializeTracks(). */ String newCue (String startTime, String endTime, String msg) { if (useVTTCue) { try { return new VTTCue(startTime, endTime, msg); } catch (Exception e) { alert("Error: new VTTCue error: " + e + "\nArguments(" + startTime + "," + endTime + "," + msg + ")"); } } else try { return new TextTrackCue(startTime, endTime, msg); } catch (Exception e) { alert("Error: new TextTrackCue error: " + e + "\nArguments(" + startTime + "," + endTime + "," + msg + ")"); } } // newCue /** * Delete the cue with whose text is specified. We will search for the cue * because some browsers number the cues differently than other browsers. */ void deleteCue(String msg) { int i; for (i = 0; i < aTrack.cues.length; i++) { if (aTrack.cues[i].text == msg) { aTrack.removeCue(aTrack.cues[i]); return; } } } // deleteCue /** * Deletes a cue given its id. * the getCueById function appears in the 28 October 2014 draft but does * seem to implemented yet. */ void deleteCueId(String id) { Object cue = aTrack.getCueById(id); aTrack.removeCue(cue); } // deleteCueId /** * Modify the Spanish track by adding and deleting cues */ void modifySpanish() { if (useVTTCue) { // Note: Internet Explorer 11 does not allow adding cues to // an existing VTT file using addCue and new TextTrackCue msgStr += NEW_LINE + "***Modifying the Spanish track"; try { // This doesn't work in IE 11 aTrack.addCue(newCue(21.000, 21.999, "veinte y dos")); aTrack.addCue(newCue(20.000, 20.999, "veinte y one")); msgStr += INDENT_NEW_LINE + "Added two cues to Spanish track"; } catch (Exception cueErr) { msgStr += INDENT_NEW_LINE + "Error in modifySpanish: Unable to add newCue: " + cueErr; } } try { // IE 11, Firefox 34, Opera 26, and Chrome 39 do not support // getCueById( ) deleteCueId("sp21"); msgStr += INDENT_NEW_LINE + "Deleted cue by id from Spanish track"; } catch (Exception e) { deleteCue("delete this cue"); msgStr += INDENT_NEW_LINE + "Deleted one cue by message from Spanish track"; } showInfo("modifySpanish"); } // modifySpanish /** * bookingkeeping */ void KISVidTBookKeeping(int trackNum, String source) { // some bookkeeping required for KISVidT.pde trackNo = trackNum; aTrack = arrayTracks[trackNo]; useTrack[videoNo][languageNo] = trackNo; trackSource[videoNo][languageNo] = source; aTrack.mode = "showing"; // now show the track cues = aTrack.cues; // Make changes in cues (Make sure this after VTT file has been read) modifyCues(); msgATrack(); msgCue(); // show cue[0] listCues(); // create list of cues showInfo("bookkeeping"); } /** * List some basic items */ void msgDefaults() { msgStr += NEW_LINE + "***Defaults"; msgStr += INDENT_NEW_LINE + "videoNo: " + videoNo; msgStr += INDENT_NEW_LINE + "trackNo: " + trackNo; msgStr += INDENT_NEW_LINE + "trackId: " + trackId; showInfo("msgDefaults"); } // msgDefaults /** * list the properties of the array of tracks */ void msgArrayTracks() { msgStr += NEW_LINE + "***Track array information"; msgStr += INDENT_NEW_LINE + "arrayTracks: " + arrayTracks; } // msgArrayTracks void msgArrayTracksList() { var i; msgStr += NEW_LINE + "***Track array Listing"; msgStr += INDENT_NEW_LINE + "arrayTracks.length: " + arrayTracks.length; try { for (i = 0; i < arrayTracks.length; i++) { msgStr += INDENT_NEW_LINE + "arrayTracks[" + i + "], label: " + arrayTracks[i].label + ", id: " + arrayTracks[i].id; try { msgStr += ", number cues: " + arrayTracks[i].cues.length; } catch (Exception e) { msgStr += ", No cues"; } } }catch (Exception e) { alert("Error getting id: " + e + "\ni: " + i); } showInfo("msgArrayTracks"); } // msgArrayTracksList /** * list the properties of the tag */ void msgHtmlTrack(Track theTrack) { Track theTrack; if (theTrack == null) theTrack = htmlTrack; msgStr += NEW_LINE + "***HTML Track Tag"; msgStr += INDENT_NEW_LINE + "info for trackId: " + trackId + " (" + trackLangs[videoNo][languageNo] + ")"; try { msgStr += INDENT_NEW_LINE + "kind: " + theTrack.kind; msgStr += INDENT_NEW_LINE + "labels: " + theTrack.label; msgStr += INDENT_NEW_LINE + "srclang: " + theTrack.srclang; msgStr += INDENT_NEW_LINE + "src: " + theTrack.src; msgStr += INDENT_NEW_LINE + "readyState: " + theTrack.readyState; msgStr += INDENT_NEW_LINE + "track: " + theTrack.track; } catch (Exception e) { msgStr += INDENT_NEW_LINE + "No <track...> tag exists for this track"; } showInfo("msgHtmlTrack"); } // msgHtmlTrack /** * list the properties of the current track */ void msgATrack() { try { msgStr += NEW_LINE + "***Current track"; msgStr += INDENT_NEW_LINE + "trackSource: " + trackSource[videoNo][languageNo]; if (aTrack != null) { msgStr += INDENT_NEW_LINE + "kind: " + aTrack.kind; msgStr += INDENT_NEW_LINE + "label: " + aTrack.label; msgStr += INDENT_NEW_LINE + "track: " + aTrack.track; msgStr += INDENT_NEW_LINE + "language: " + aTrack.language; msgStr += INDENT_NEW_LINE + "mode: " + aTrack.mode; msgStr += INDENT_NEW_LINE + "cues: " + aTrack.cues; if (cues != null) msgStr += INDENT_NEW_LINE + "cues.length: " + aTrack.cues.length; msgStr += INDENT_NEW_LINE + "id: " + aTrack.id; } } catch (Exception e) { msgStr += INDENT_NEW_LINE + "Error: MsgATrack: " + e; } showInfo("msgATrack"); } // msgATrack /** * Just announce that cues from an existing track will be used */ void msgUseExistingTrack() { msgStr += NEW_LINE + "***Using an existing track"; } // msgUseExistingTrack /** * Just announce that cues from an existing track will be used */ void msgUseStandardMethod() { msgStr += NEW_LINE + "***Using an existing track"; } // msgUseExistingTrack /** * Just announce the cues are being generaged by Processing.js code */ void msgGenerateCodeCue() { msgStr += NEW_LINE + "***Generating cues with Processing.js code"; showInfo("msgGenerateCodeCue"); } // msgGenerateCodeCue /** * Just announce that the cues are being read from the HTML file */ void msgReadHTML() { msgStr += NEW_LINE + "***Reading cues from HTML file"; showInfo("msgHeadHTML"); } // msgReadHTML /** * use cue[0] for the current track */ void msgCue() { try { msgStr += NEW_LINE + "***Cue[0]"; cue = cues[0]; msgStr += INDENT_NEW_LINE + "cue: " + cue; msgStr += INDENT_NEW_LINE + "cue.id: " + cue.id; msgStr += INDENT_NEW_LINE + "cue startTime: " + cue.startTime; msgStr += INDENT_NEW_LINE + "cue endTime: " + cue.endTime; msgStr += INDENT_NEW_LINE + "cue text: " + cue.text; try {msgStr += INDENT_NEW_LINE + "0 - Region: " + cue.region; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "1 - vertical: " + cue.vertical; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "2 - snapToLines: " + cue.snapToLines; } catch (Exception e) { msgStr += INDENT_NEW_LINE + "2 - snapToLines: " + e; } try {msgStr += INDENT_NEW_LINE + "3 - line: " + cue.line; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "4 - lineAlign: " + cue.lineAlign; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "5 - position: " + cue.position; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "6 - positionAlign: " + cue.positionAlign; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "7 - size: " + cue.size; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "8 - align: " + cue.align; } catch (Exception e) {} try {msgStr += INDENT_NEW_LINE + "9 - getCueAsHTML: " + cue.getCueAsHTML; } catch (Exception e) {} showInfo("msgCue"); } catch (Exception e) { msgStr += NEW_LINE + "***Error in msgCue: " + e; } getAllProperties(cue, "cues[0]"); } // msgCue /** * list all the cues for the current track. They will show up on the * right side of the page */ void listCues() { int i; String s; try { cueStr = "" + ""; for (i = 0; i < cues.length; i++) { s = replaceAll(cues[i].text, "<", "<"); s = replaceAll(s, ">", ">"); cueStr += ""; } cueStr += "
#idBeginEndText
" + i + "" + cues[i].id + "" + cues[i].startTime + "" + cues[i].endTime + "" + s /*cues[i].text*/ + "
"; } catch (Exception e) { cueStr += "err: " + err + ""; } showInfo("listCues"); } //listCues void showInfo(String title) { data = "
" + title + "
" + msgStr + "
" + cueStr + "
"; if (showTheInfo) document.getElementById("info").innerHTML = data; else document.getElementById("info").innerHTML = ""; } // showInfo void getAllProperties(Object obj, String objName) { String properties = ""; for (property in obj) { try { properties += "
" + property + " " + obj[property]; } catch (Exception e) { properties += "
" + property + " " + e; } } println("Properties of object: " + objName + properties); properties = replaceAll(properties, "
", "\n"); alert(properties); }