// Ve API Script 1.0 // Videncode API var Videncode = (function () { // Variables var signature = ".ve.snd\0"; /** Constructor method. @return new Videncode object */ function ve() { // Initial state this.data_blob = null; this.reset(); } // Private methods var this_private = { /** Keep a string to be less than maxLen characters when encoded as UTF-8. @param str the string to modify @param maxLen the maximum length of the string when encoded in UTF-8 @return the string updated to be within maxLen's length range */ adjust_utf8_string_length: function (str, maxLen) { var utf8_str = unescape(encodeURIComponent(str)); if (utf8_str.length > maxLen) { var loop = true; var newStr = ""; while (loop && maxLen >= 0) { loop = false; try { newStr = decodeURIComponent(escape(utf8_str.substr(0, maxLen))); } catch (e) { --maxLen; loop = true; } } return newStr; } return str; }, /** Set the error message if it is not set. @param message a string containing an error message @return this */ set_error: function (message) { // Set if (this.error_message === null) { this.error_message = message; } return this; }, /** Get the number of bytes required to store a varlen int. @param value the value to be stored @param maxLen the maximum number of bytes @return the number of bytes needed */ get_varlen_int_length: function (value, maxLen) { var i = 0; for (; i < maxLen; ++i) { value = value >>> 7; if (value == 0) return i + 1; } return i; }, /** Convert an integer to a varlen int, stored in a Uint8Array. @param value the value to be stored @param bytes a Uint8Array which will contain the result; it's length is used as the max length @return the number of bytes useds */ int_to_varlen_bytes: function (value, bytes) { var i = 0; for (; i < bytes.length; ++i) { if (i > 0) bytes[i - 1] |= 0x80; bytes[i] = (value & 0x7F); value = value >>> 7; if (value == 0) return i + 1; } return i; }, /** Convert a string to a Uint8Array. Note that this does not do any UTF-8 conversion, that has to be done beforehand. @param str the string to convert @return a new Uint8Array containing the string */ string_to_bytes: function (str) { var bytes = new Uint8Array(new ArrayBuffer(str.length)) for (var i = 0; i < str.length; ++i) bytes[i] = str.charCodeAt(i); return bytes; }, /** Mask a single byte and return its new value. If masking is disabled, this does nothing and returns the input. @param b the byte to mask @return the masked byte */ mask_byte: function (b) { if (!this.mask_file) return b; this.mask_value = (this.mask_value * 102293 + 390843) & 0xFFFFFFFF; this.mask = this.mask_value >>> 24; this.mask_value += b; return (b ^ this.mask); }, /** Update the mask with a sequence of bytes. @param bytes a Uint8Array of byte values @param length the number of bytes to update the mask with */ mask_modify_from_bytes: function (bytes, length) { if (!this.mask_file) return; for (var i = 0; i < length; ++i) { this.mask_value = (this.mask_value * 102293 + 390843) & 0xFFFFFFFF; this.mask = this.mask_value >>> 24; this.mask_value += ((bytes[i] & 0xFF) ^ this.mask); } } }; // Public Methods ve.prototype = { constructor: ve, /** Reset the state of the object. @return this */ reset: function () { this.error_message = null; this.video = null; this.audio = null; this.image = null; this.image_mime = ""; this.sync_offset = 0.0; this.video_fades = [ false , false ]; this.audio_fades = [ false , false ]; this.video_play_style = [ 0 , 0 ]; this.audio_play_style = [ 0 , 0 ]; this.output_data = null; if (this.data_blob != null) { this.data_blob = null; (window.webkitURL || window.URL).revokeObjectURL(this.data_blob_url); } this.data_blob_url = null; this.mask_file = true; this.mask = 0x12; this.mask_value = 0xABCDEF; return this; }, /** Encode the given settings into a new file. To check if this succeeded, use has_error() after the call. @return this */ encode: function () { // Check if (this.error_message !== null || this.output_data !== null) { this_private.set_error.call(this, "Not reset"); return this; } if (this.image == null) { this_private.set_error.call(this, "No image"); return this; } if (this.video == null && this.audio == null) { this_private.set_error.call(this, "No video or audio"); return this; } // Signature var signature_array = this_private.string_to_bytes.call(this, signature); // UTF-8 tag var utf8_tag = (this.tag == null ? "" : this_private.adjust_utf8_string_length.call(this, this.tag, 100)); utf8_tag = unescape(encodeURIComponent(utf8_tag)); var utf8_tag_array = this_private.string_to_bytes.call(this, utf8_tag); // Sync offset var sync_int = Math.floor(this.sync_offset); var sync_dec = this.sync_offset - sync_int; // Temp var temp = Uint8Array(new ArrayBuffer(5)); var has_both = (this.video != null && this.audio != null); // Calculate size requirements var size = this.image.length + // image signature_array.length + // signature 1 + // version 1 + // flags1 (has_both ? 1 : 0) + // flags2 this_private.get_varlen_int_length.call(this, utf8_tag_array.length, 5) + // tag length utf8_tag_array.length + // tag (has_both ? this_private.get_varlen_int_length.call(this, sync_int, 5) + 2 : 0) + // sync offset (this.video != null ? this_private.get_varlen_int_length.call(this, this.video.length, 5) : 0) + // video length (this.video != null ? this.video.length : 0) + // video (this.audio != null ? this_private.get_varlen_int_length.call(this, this.audio.length, 5) : 0) + // audio length (this.audio != null ? this.audio.length : 0); // audio // Create new this.output_data = Uint8Array(new ArrayBuffer(size)); // Copy image var pos = 0; this_private.mask_modify_from_bytes.call(this, this.image, this.image.length); for (; pos < this.image.length; ++pos) { this.output_data[pos] = this.image[pos]; } // Signature for (var i = 0; i < signature_array.length; ++i) { this.output_data[pos] = this_private.mask_byte.call(this, signature_array[i]); ++pos; } // Version temp[0] = 1; this.output_data[pos++] = this_private.mask_byte.call(this, temp[0]); // Flags1 temp[0] = ( (this.video != null ? 0x01 : 0x00) | (this.audio != null ? 0x02 : 0x00) | // 0x04 : reserved // 0x08 : reserved (has_both && this.video_fades[0] ? 0x10 : 0x00) | (has_both && this.video_fades[1] ? 0x20 : 0x00) | (has_both && this.audio_fades[0] ? 0x40 : 0x00) | (has_both && this.audio_fades[1] ? 0x80 : 0x00) ); this.output_data[pos++] = this_private.mask_byte.call(this, temp[0]); // Flags2 if (has_both) { temp[0] = ( (this.video_play_style[0] & 0x03) | // 0x01 , 0x02 ((this.video_play_style[1] & 0x03) << 2) | // 0x04 , 0x08 ((this.audio_play_style[0] & 0x01) << 4) | // 0x10 ((this.audio_play_style[0] & 0x01) << 5) // 0x20 // 0x40 : reserved // 0x80 : reserved ); this.output_data[pos++] = this_private.mask_byte.call(this, temp[0]); } // Tag var len = this_private.int_to_varlen_bytes.call(this, utf8_tag_array.length, temp); for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, temp[i]); } for (var i = 0; i < utf8_tag_array.length; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, utf8_tag_array[i]); } // Sync offset if (has_both) { len = this_private.int_to_varlen_bytes.call(this, sync_int, temp); for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, temp[i]); } len = 2; for (var i = 0; i < len; ++i) { temp[i] = 0; for (var j = 0; j < 8; ++j) { if ((sync_dec *= 2) >= 1.0) { sync_dec -= 1.0; temp[i] |= (1 << j); } } } for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, temp[i]); } } // Video if (this.video != null) { len = this_private.int_to_varlen_bytes.call(this, this.video.length, temp); for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, temp[i]); } len = this.video.length; for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, this.video[i]); } } // Audio if (this.audio != null) { len = this_private.int_to_varlen_bytes.call(this, this.audio.length, temp); for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, temp[i]); } len = this.audio.length; for (var i = 0; i < len; ++i) { this.output_data[pos++] = this_private.mask_byte.call(this, this.audio[i]); } } // Done return this; }, /** Get the byte array of the final image. @return null if the encoding wasn't completed otherwise, a Uint8Array of the data */ get_data: function () { return this.output_data; }, /** Get a usable blob URL from the generated data. @return null if the encoding wasn't completed otherwise, a string containing a URL */ get_url: function () { if (this.output_data != null) { if (this.data_blob == null) { this.data_blob = new Blob([ this.output_data ], {type: this.image_mime}); this.data_blob_url = (window.webkitURL || window.URL).createObjectURL(this.data_blob); } return this.data_blob_url; } return null; }, /** Get the mime type of the original image. @return whatever was passed into set_image(); presumably one of "image/jpeg", "image/png", or "image/gif" */ get_image_mime_type: function () { return this.image_mime; }, /** Get the error message. @return a string containing the error message, or null if no error */ get_error: function () { // Get return (this.error_message !== null ? this.error_message : (this.output_data === null ? "Not encoded" : null)); }, /** Check if there was an error. @return true if there was an error, false otherwise */ has_error: function () { return (this.error_message !== null || this.output_data === null); }, /** Set the video data for the object. @param video a Uint8Array of the video data @return this */ set_video: function (video) { this.video = video; return this; }, /** Set the audio data for the object. @param audio a Uint8Array of the audio data @return this */ set_audio: function (audio) { this.audio = audio return this; }, /** Set the image data for the object. @param image a Uint8Array of the image data @param mime_type a string of the mime type of the image @return this */ set_image: function (image, mime_type) { this.image = image; this.image_mime = mime_type; return this; }, /** Set tag for the object. @param tag a string of the tag @return this */ set_tag: function (tag) { this.tag = tag; return this; }, /** Set the method the video should play with. This is only meaningful if the video and audio are separate. Current values are: 0: display blank when the video isn't playing 1: loop the video 2: display the video frame when the video isn't playing 3: display the image when the video isn't playing @param start true if the method should be applied to the start of playback, false otherwise @param style one of the above values @return this */ set_video_play_style: function (start, style) { if (style !== 0 && style !== 1 && style !== 2 && style !== 3) style = 0; this.video_play_style[start ? 0 : 1] = style; return this; }, /** Set the method the audio should play with. This is only meaningful if the video and audio are separate. Current values are: 0: play nothing 1: loop the audio @param start true if the method should be applied to the start of playback, false otherwise @param style one of the above values @return this */ set_audio_play_style: function (start, style) { if (style !== 0 && style !== 1) style = 0; this.audio_play_style[start ? 0 : 1] = style; return this; }, /** Set if the video should fade in/out or not. This is only meaningful if the video and audio are separate. @param start true if the fade should be applied to the start of playback, false otherwise @param enabled true if a fade should occur, false otherwise @return this */ set_video_fade: function (start, enabled) { this.video_fades[start ? 0 : 1] = enabled; return this; }, /** Set if the audio should fade in/out or not. This is only meaningful if the video and audio are separate. @param start true if the fade should be applied to the start of playback, false otherwise @param enabled true if a fade should occur, false otherwise @return this */ set_audio_fade: function (start, enabled) { this.audio_fades[start ? 0 : 1] = enabled; return this; }, /** Set the sync offset of the shorter track. This is only meaningful if the video and audio are separate. @param offset the offset in seconds @return this */ set_sync_offset: function (offset) { this.sync_offset = offset; return this; } }; // Return return ve; })(); // Videcode API var Videcode = (function () { // Variables var function_type = typeof(function(){}); var object_type = typeof({}); var signature = ".ve.snd\0"; var signature_array = new Uint8Array(new ArrayBuffer(signature.length)); for (i = 0; i < signature.length; ++i) signature_array[i] = signature.charCodeAt(i); /** Constructor method. @param source a Uint8Array of the full image @param filename the name of the original file @return new Videcode object */ function ve(source, filename) { // Set vars this.source = source; this.filename = filename; var ext = filename.split(".").pop().toLowerCase(); if (ext == "png") this.mime = "image/png"; else if (ext == "gif") this.mime = "image/gif"; else this.mime = "image/jpeg" // Status this.async_timer = null; this.reset(); } // Private methods var this_private = { /** Update the mask state with a single byte value. @param b the byte value @return the unmasked byte */ mask_update: function (b) { // Mask update this.mask_value = (this.mask_value * 102293 + 390843) & 0xFFFFFFFF; this.mask = this.mask_value >>> 24; b = (b ^ this.mask); this.mask_value += b; // Return unmasked value return b; }, /** Update the mask state with a single byte value. A checking version which returns -1 if the end of the file has been reached and will modify the error message. @param b the byte value @return the unmasked byte, or -1 if at EOF */ mask_update_checked: function (b) { // Check for undefined if (b === undefined) { this_private.set_error.call(this, "End of file"); return -1; } // Mask update this.mask_value = (this.mask_value * 102293 + 390843) & 0xFFFFFFFF; this.mask = this.mask_value >>> 24; b = (b ^ this.mask); this.mask_value += b; // Return unmasked value return b; }, /** Read a variable-byte-length integer from the source. On failure, the error message will be modified. @param start the position of the first byte @param maxlen the maximum number of bytes that can make up the number @return null on failure, [ value , count ] on success, where value is the retrieved integer value, count is the number of bytes read */ read_varlen_int: function (start, maxlen) { var value = 0; var i = 0, b; // Read for (; i < maxlen; ++i) { value = value | ((b = this_private.mask_update_checked.call(this, this.source[start + i])) & 0x7F) << (7 * i); if (b < 0) { // End of stream return null; } if ((b & 0x80) == 0) break; } // Bad format if (i == maxlen) { this_private.set_error.call(this, "Bad number format"); return null; } // Okay return [ value , i + 1 ]; }, /** Set the error message if it is not set. @param message a string containing an error message @return this */ set_error: function (message) { // Set if (this.error_message === null) { this.error_message = message; this.malformed = true; } return this; }, /** */ decode_async_internal: function (async_settings, callback, progress, callback_data, sd) { // Vars var self = this; this.async_timer = null; var len = this.source.length, b, data; // Reset if (sd.reset === undefined) { this.reset(); sd.i = 0; sd.j = 0; sd.pos = 0; sd.reset = true; } var i_start = sd.i; // Find signature if (sd.signature_check_done === undefined) { while (sd.i < len) { b = this_private.mask_update.call(this, this.source[sd.i]); if (b == signature_array[sd.pos]) { // Found if (++sd.pos >= signature_array.length) { ++sd.i; this.data_offset = sd.i - signature_array.length; break; } } else { // Reset sd.pos = 0; } // Async if ((++sd.i) - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } } sd.signature_check_done = true; } // No errors if (this.error_message === null) { // Signature found if (sd.i < len) { // Version if (sd.version === undefined) { this.version = this_private.mask_update_checked.call(this, this.source[sd.i++]); sd.version = true; } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // Flags1 if (sd.flags1 === undefined) { sd.flags1 = this_private.mask_update_checked.call(this, this.source[sd.i++]); this.multiplexed = ((sd.flags1 & 0x04) != 0); this.video_fades[0] = ((sd.flags1 & 0x10) != 0); this.video_fades[1] = ((sd.flags1 & 0x20) != 0); this.audio_fades[0] = ((sd.flags1 & 0x40) != 0); this.audio_fades[1] = ((sd.flags1 & 0x80) != 0); } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // Flags2 if (sd.flags2 === undefined) { sd.flags2 = 0; if ((sd.flags1 & 0x03) == 0x03) { sd.flags2 = this_private.mask_update_checked.call(this, this.source[sd.i++]); this.video_play_style[0] = (sd.flags2 & 0x03); this.video_play_style[1] = (sd.flags2 & 0x0C) >> 2; this.audio_play_style[0] = (sd.flags2 & 0x10) >> 4; this.audio_play_style[1] = (sd.flags2 & 0x20) >> 5; this.video_is_longer = ((sd.flags2 & 0x40) != 0); } } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // Var-length tag if (sd.tag_found === undefined) { // Length if (sd.tag_length === undefined) { data = this_private.read_varlen_int.call(this, sd.i, 5); if (data !== null) { sd.i += data[1]; sd.tag_length = data[0]; } else { sd.tag_length = -1; } if (sd.tag_length >= 0) { if (sd.tag_length + sd.i <= this.source.length) { sd.i_end = sd.i + sd.tag_length; } else { // Error this_private.set_error.call(this, "End of file"); } } } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // No errors if (this.error_message === null) { // Tag while (sd.i < sd.i_end) { this.tag += String.fromCharCode(this_private.mask_update.call(this, this.source[sd.i])); // Async if ((++sd.i) - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } } // Decode UTF-8 try { this.tag = decodeURIComponent(escape(this.tag)); } catch (e) {} } sd.tag_found = true; } // No errors if (this.error_message === null) { // Sync offsets if (sd.sync_offset === undefined) { if ((sd.flags1 & 0x03) == 0x03) { // Integer part data = this_private.read_varlen_int.call(this, sd.i, 5); if (data != null) { sd.i += data[1]; this.sync_offset = data[0]; // Decimal part var dec = [ 0 , 0 ]; var dec_val = 0.5; var fraction = 0.0; dec[0] = this_private.mask_update_checked.call(this, this.source[sd.i++]); dec[1] = this_private.mask_update_checked.call(this, this.source[sd.i++]); for (var j = 0; j < dec.length; ++j) { for (var k = 0; k < 8; ++k) { if ((dec[j] & (1 << k)) != 0) fraction += dec_val; dec_val /= 2.0; } } this.sync_offset += fraction; } } sd.sync_offset = true; } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // No errors if (this.error_message === null) { // Video if (sd.video_found === undefined) { if ((sd.flags1 & 0x01) != 0) { // Length if (sd.video_length === undefined) { data = this_private.read_varlen_int.call(this, sd.i, 5); if (data !== null) { sd.i += data[1]; sd.video_length = data[0]; } else { sd.video_length = -1; } if (sd.video_length >= 0) { if (sd.video_length + sd.i <= this.source.length) { this.video = new Uint8Array(new ArrayBuffer(sd.video_length)); sd.j = 0; sd.i_end = sd.i + sd.video_length; } else { // Error this_private.set_error.call(this, "End of file"); } } } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // No errors if (this.error_message === null) { // Video data while (sd.i < sd.i_end) { this.video[sd.j++] = this_private.mask_update.call(this, this.source[sd.i]); // Async if ((++sd.i) - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } } } } sd.video_found = true; } // No errors if (this.error_message === null) { // Audio if (sd.audio_found === undefined) { if ((sd.flags1 & 0x02) != 0) { // Length if (sd.audio_length === undefined) { data = this_private.read_varlen_int.call(this, sd.i, 5); if (data !== null) { sd.i += data[1]; sd.audio_length = data[0]; } else { sd.audio_length = -1; } // Checking if (sd.audio_length >= 0) { if (sd.audio_length + sd.i <= this.source.length) { this.audio = new Uint8Array(new ArrayBuffer(sd.audio_length)); sd.j = 0; sd.i_end = sd.i + sd.audio_length; } else { this_private.set_error.call(this, "End of file"); } } } // Async if (sd.i - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } // No errors if (this.error_message === null) { // Video data while (sd.i < sd.i_end) { this.audio[sd.j++] = this_private.mask_update.call(this, this.source[sd.i]); // Async if ((++sd.i) - i_start >= async_settings.steps) { if (progress != null) progress.call(this, {percent: (sd.i / len)}, callback_data); this.async_timer = setTimeout(function () { this_private.decode_async_internal.call(self, async_settings, callback, progress, callback_data, sd); }, async_settings.delay); return; } } } } sd.audio_found = true; } // Image this.image = this.source.subarray(0, this.data_offset); } } } } else { this_private.set_error.call(this, "No data found"); this.malformed = false; } } // Error if (this.error_message !== null) { this.video = null; this.audio = null; this.image = null; } // Async: done if (progress != null) { progress.call(this, {percent: 1.0}, callback_data); } if (callback != null) { callback.call(this, callback_data); } } }; // Public Methods ve.prototype = { constructor: ve, /** Reset the state of the object. @return this */ reset: function () { // Decoding data this.version = 0; this.malformed = false; this.error_message = null; this.mask = 0x12; this.mask_value = 0xABCDEF; this.data_offset = 0; this.tag = ""; this.sync_offset = 0; this.multiplexed = false; this.video_is_longer = false; this.video = null; this.audio = null; this.image = null; if (this.async_timer != null) { clearTimeout(this.async_timer); this.async_timer = null; } this.video_fades = [ false , false ]; this.audio_fades = [ false , false ]; this.video_play_style = [ 0 , 0 ]; this.audio_play_style = [ 0 , 0 ]; return this; }, /** Decode the source image; image is guaranteed to be decoded on return. @return this */ decode: function () { // Reset this.reset(); // Setup var pos = 0; var len = this.source.length; var b, i; // Find signature for (i = 0; i < len; ++i) { b = this_private.mask_update.call(this, this.source[i]); if (b == signature_array[pos]) { // Found if (++pos >= signature_array.length) { ++i; this.data_offset = i - signature_array.length; break; } } else { // Reset pos = 0; } } // Signature found if (i < len) { // Version this.version = this_private.mask_update_checked.call(this, this.source[i++]); // Flags1 var flags1 = this_private.mask_update_checked.call(this, this.source[i++]); this.multiplexed = ((flags1 & 0x04) != 0); this.video_fades[0] = ((flags1 & 0x10) != 0); this.video_fades[1] = ((flags1 & 0x20) != 0); this.audio_fades[0] = ((flags1 & 0x40) != 0); this.audio_fades[1] = ((flags1 & 0x80) != 0); // Flags2 var flags2 = 0; if ((flags1 & 0x03) == 0x03) { flags2 = this_private.mask_update_checked.call(this, this.source[i++]); this.video_play_style[0] = (flags2 & 0x03); this.video_play_style[1] = (flags2 & 0x0C) >> 2; this.audio_play_style[0] = (flags2 & 0x10) >> 4; this.audio_play_style[1] = (flags2 & 0x20) >> 5; this.video_is_longer = ((flags2 & 0x40) != 0); } // Var-length tag var data = this_private.read_varlen_int.call(this, i, 5); if (data !== null) { i += data[1]; var tag_length = data[0]; // Tag if (tag_length + i <= this.source.length) { len = i + tag_length; for (; i < len; ++i) { this.tag += String.fromCharCode(this_private.mask_update.call(this, this.source[i])); } // Decode UTF-8 try { this.tag = decodeURIComponent(escape(this.tag)); } catch (e) {} } else { // Error this_private.set_error.call(this, "End of file"); } } // Sync offsets if ((flags1 & 0x03) == 0x03 && this.error_message === null) { // Integer part var data = this_private.read_varlen_int.call(this, i, 5); if (data != null) { i += data[1]; this.sync_offset = data[0]; // Decimal part var dec = [ 0 , 0 ]; var dec_val = 0.5; var fraction = 0.0; dec[0] = this_private.mask_update_checked.call(this, this.source[i++]); dec[1] = this_private.mask_update_checked.call(this, this.source[i++]); for (var j = 0; j < dec.length; ++j) { for (var k = 0; k < 8; ++k) { if ((dec[j] & (1 << k)) != 0) fraction += dec_val; dec_val /= 2.0; } } this.sync_offset += fraction; } } // Video if ((flags1 & 0x01) != 0 && this.error_message === null) { data = this_private.read_varlen_int.call(this, i, 5); if (data !== null) { i += data[1]; var video_length = data[0]; // Video data if (video_length + i <= this.source.length) { len = i + video_length; this.video = new Uint8Array(new ArrayBuffer(video_length)); var j = 0; for (; i < len; ++i) { this.video[j++] = this_private.mask_update.call(this, this.source[i]); } } else { // Error this_private.set_error.call(this, "End of file"); } } } // Audio if ((flags1 & 0x02) != 0 && this.error_message === null) { data = this_private.read_varlen_int.call(this, i, 5); if (data !== null) { i += data[1]; var audio_length = data[0]; // Video data if (audio_length + i <= this.source.length) { len = i + audio_length; this.audio = new Uint8Array(new ArrayBuffer(audio_length)); var j = 0; for (; i < len; ++i) { this.audio[j++] = this_private.mask_update.call(this, this.source[i]); } } else { // Error this_private.set_error.call(this, "End of file"); } } } // Image this.image = this.source.subarray(0, this.data_offset); } else { this_private.set_error.call(this, "No data found"); this.malformed = false; } // Error if (this.error_message !== null) { this.video = null; this.audio = null; this.image = null; } // Done return this; }, /** Decode the source image; image is guaranteed to be decoded on return. @param [async_settings] optional; object containing the asynchronous settings @param callback a callback to be executed, in the form of: function (callback_data) {...} where the this object = the videcode object callback_data is the last argument @param [progress] a callback to be executed on status updates, in the form of: function (progress_data, callback_data) {...} where the this object = the videcode object progress_data is in the form of {percent:?} callback_data is the next argument @param [callback_data] optional; data to be passed as the first argument into the callback function */ decode_async: function (async_settings, callback, progress, callback_data) { // Arguments if (arguments.length <= 1 || typeof(async_settings) == function_type) { // Shift if omitted callback_data = progress; progress = callback; callback = async_settings; async_settings = null; } if (typeof(callback) != function_type) { callback = null } if (typeof(progress) != function_type) { progress = null } // Async settings if (async_settings == null || typeof(async_settings) != object_type) { async_settings = {}; } async_settings.steps = async_settings.steps || 100; async_settings.delay = async_settings.delay || 100; if (async_settings.steps < 0) async_settings.steps = 0; if (async_settings.delay < 1) async_settings.delay = 1; // Call this_private.decode_async_internal.call(this, async_settings, callback, progress, callback_data, {}); }, /** Get the error message. @return a string containing the error message, or null if no error */ get_error: function () { // Get return (this.error_message !== null ? this.error_message : (this.image === null ? "Not decoded" : null)); }, /** Check if there was an error. @return true if there was an error, false otherwise */ has_error: function () { return (this.error_message !== null || this.image === null); }, /** Check if the decoded data was malformed. @return true if not decoded or not malformed, false if an error occured that wasn't "no data found" */ is_malformed: function () { return this.malformed; }, /** Check if this object has video. @return true if it has video data, false otherwise */ has_video: function () { return (this.video != null); }, /** Check if this object has separate audio. This will return false if the audio is multiplexed into the video. @return true if it has separate audio data, false otherwise */ has_audio: function () { return (this.multiplexed || this.audio != null); }, /** Check if this object has audio and video multiplexed together. @return true if it has audio and video multiplexed together, false otherwise */ is_muxed: function () { return this.multiplexed; }, /** Get the video data. @return null if there is no video, otherwise, a Uint8Array of the video file */ get_video: function () { return this.video; }, /** Get the separate audio data. @return null if there is no separate audio, otherwise, a Uint8Array of the audio file */ get_audio: function () { return this.audio; }, /** Get the image data. @return a Uint8Array of the image file */ get_image: function () { return this.image; }, /** Get the source data. @return a Uint8Array of the source file */ get_source: function () { return this.source; }, /** Returns the file tag. @return a string containing the tag */ get_tag: function () { return this.tag; }, /** Returns the synchronization offset. @return a number in seconds of the offset */ get_sync_offset: function () { return this.sync_offset; }, /** Get the mime type of the original image. @return either "image/jpeg", "image/png", or "image/gif" */ get_image_mime_type: function () { return this.mime; }, /** Return if video fading is enabled for the checked value. This is only meaningful if the video and audio are separate. @param start true if checking when the video starts playing, false if checking at the end @return true if enabled, false otherwise */ get_video_fade: function (start) { return (this.video_fades[start ? 0 : 1]); }, /** Return if audio fading is enabled for the checked value. This is only meaningful if the video and audio are separate. @param start true if checking when the audio starts playing, false if checking at the end @return true if enabled, false otherwise */ get_audio_fade: function (start) { return (this.audio_fades[start ? 0 : 1]); }, /** Returns the method the video should play with. This is only meaningful if the video and audio are separate. Current values are: 0: display blank when the video isn't playing 1: loop the video 2: display the video frame when the video isn't playing 3: display the image when the video isn't playing @param start true if checking when the video starts playing, false if checking at the end @return one of the above values */ get_video_play_style: function (start) { return (this.video_play_style[start ? 0 : 1]); }, /** Returns the method the audio should play with. This is only meaningful if the video and audio are separate. Current values are: 0: play nothing 1: loop the audio @param start true if checking when the audio starts playing, false if checking at the end @return one of the above values */ get_audio_play_style: function (start) { return (this.audio_play_style[start ? 0 : 1]); }, /** Check if the object has both audio and video. @return true if it has both, false otherwise */ has_video_and_audio: function () { return (this.audio != null && this.video != null); } }; // Return return ve; })(); // Playback API var VPlayer = (function () { // Variables var function_type = typeof(function(){}); var DISPLAY_NOTHING = 0; var DISPLAY_LOOPED = 1; var DISPLAY_VIDEO = 2; var DISPLAY_IMAGE = 3; var PLAY_NOTHING = 0; var PLAY_LOOPED = 1; /** Constructor method. @param [videcode] a Videcode object which has been properly initialized if omitted, gen_data must be called later with a Videcode object @return new VPlayer object */ function vp(videcode) { // Set vars this.videcode = (videcode === undefined ? null : videcode); // Animation data this.video_animation_time = [ 1.0 , 1.0 ]; // seconds this.audio_animation_time = [ 1.0 , 1.0 ]; // seconds this.audio_animation_interval = 50; // ms this.video_desync_max = 0.25; // seconds this.audio_desync_max = 0.25; // seconds var css_styles = [ "transition" , "webkitTransition" , "MozTransition" , "OTransition" , "msTransition" ]; var good_type = typeof(""); var d = document.createElement("div"); for (var i = 0; i < css_styles.length; ++i) { if (typeof(d.style[css_styles[i]]) == good_type || (i == css_styles.length - 1 && (i = 0) == 0)) { this.transition_css = css_styles[i]; this.transition_end_event_name = this.transition_css + (i == 0 ? "end" : "End"); break; } } // Playback data this.clear_listeners(); // Create data this.sync_timer = null; this.video_animate_timer = null; this.video_loop_remove_timer = null; this.video_loop_stop_timer = null; this.audio_animate_timer = null; this.audio_loop_remove_timer = null; this.audio_loop_stop_timer = null; this.element_container = null; this.video_tag = null; this.audio_tag = null; this.image_tag = null; this.video_callbacks = []; this.audio_callbacks = []; this.image_callbacks = []; this.video_blob = null; this.video_blob_url = null; this.audio_blob = null; this.audio_blob_url = null; this.image_blob = null; this.image_blob_url = null; this.gen_data(); } // Private methods var this_private = { /** Triggers an event. @param event_name the string name of the event @param data an object containing data to be passed to the event callbacks */ trigger: function (event_name, data) { var listeners = this.event_listeners[event_name]; if (listeners && listeners.length > 0) { for (var i = 0; i < listeners.length; ++i) { listeners[i].call(this, data); } } }, /** Clear all timers related to playback. */ clear_timers: function () { if (this.sync_timer != null) { clearTimeout(this.sync_timer); this.sync_timer = null; } if (this.video_animate_timer != null) { clearTimeout(this.video_animate_timer); this.video_animate_timer = null; } if (this.video_loop_remove_timer != null) { clearTimeout(this.video_loop_remove_timer); this.video_loop_remove_timer = null; clearTimeout(this.video_loop_stop_timer); this.video_loop_stop_timer = null; } else if (this.video_loop_stop_timer != null) { clearTimeout(this.video_loop_stop_timer); this.video_loop_stop_timer = null; } if (this.audio_animate_timer != null) { clearTimeout(this.audio_animate_timer); this.audio_animate_timer = null; } if (this.audio_loop_remove_timer != null) { clearTimeout(this.audio_loop_remove_timer); this.audio_loop_remove_timer = null; clearTimeout(this.audio_loop_stop_timer); this.audio_loop_stop_timer = null; } else if (this.audio_loop_stop_timer != null) { clearTimeout(this.audio_loop_stop_timer); this.audio_loop_stop_timer = null; } }, /** Internal way of playing and synchronizing separate audio/video tracks. Handles all the methods of playback. */ play_synced: function () { var self = this; if (this.video_main) { // Get the current time var current_time = this.video_tag.currentTime; if (current_time >= this.max_duration) { // Reset this.video_tag.currentTime = 0.0; current_time = 0.0; } // Animation var v = this_private.get_audio_volume_at_time.call(this, current_time); this.audio_tag.volume = this.volume * v; var min_time = (this.audio_play_style[0] == PLAY_LOOPED ? 0.0 : this.sync_offset); var max_time = (this.audio_play_style[1] == PLAY_LOOPED ? this.max_duration : this.sync_offset + this.min_duration); if (current_time >= this.sync_offset) { if (current_time < this.sync_offset + this.min_duration) { // Volume fade in/out var b = (v == 1.0); if (!b && !this.audio_fades[0]) b = true; // start fade not enabled if (!b || this.audio_fades[1]) { // fade must be enabled this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, (b ? (this.sync_offset + this.min_duration - this.audio_animation_time[1]) - current_time : this.audio_animation_interval)); } } } else { if (this.audio_fades[0]) { // Volume fade in this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, this.sync_offset - current_time); } else if (this.audio_fades[1]) { // Volume fade out this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, (this.sync_offset + this.min_duration - this.audio_animation_time[1]) - current_time); } } // Playback and sync if (current_time >= this.sync_offset) { if (this.audio_play_style[1] == PLAY_LOOPED) { // Sync var t; if (Math.abs((t = this_private.get_audio_position_at_time.call(this, current_time)) - this.audio_tag.currentTime) > this.audio_desync_max) { this.audio_tag.currentTime = t; } // Play looped this.audio_tag.loop = true; this.audio_tag.play(); } else { // Remove any looping this.audio_tag.loop = false; if (current_time < this.sync_offset + this.min_duration) { // Sync var t; if (Math.abs((t = this_private.get_audio_position_at_time.call(this, current_time)) - this.audio_tag.currentTime) > this.audio_desync_max) { this.audio_tag.currentTime = t; } // Play normally this.audio_tag.play(); } else { // Sync at end this.audio_tag.currentTime = this.audio_duration; } } } else { if (this.audio_play_style[0] == PLAY_LOOPED) { // Sync var t; if (Math.abs((t = this_private.get_audio_position_at_time.call(this, current_time)) - this.audio_tag.currentTime) > this.audio_desync_max) { this.audio_tag.currentTime = t; } // Play looped this.audio_tag.loop = true; this.audio_tag.play(); // Set timers to stop looping if (this.audio_play_style[1] != PLAY_LOOPED) { this.audio_loop_stop_timer = setTimeout(function() { this_private.on_timed_audio_loop_stop.call(self); }, (this.sync_offset + this.min_duration - current_time) * 1000); this.audio_loop_remove_timer = setTimeout(function() { this_private.on_timed_audio_loop_remove.call(self); }, (this.sync_offset + this.min_duration / 2.0 - current_time) * 1000); } } else { // Sync at 0 this.audio_tag.currentTime = 0.0; // Set timer to activate playback at the proper time this.sync_timer = setTimeout(function() { this_private.on_timed_audio_play.call(self); }, (this.sync_offset - current_time) * 1000); } } // Play video this.video_tag.play(); } else { // Get the current time var current_time = this.audio_tag.currentTime; if (current_time >= this.max_duration) { // Reset this.audio_tag.currentTime = 0.0; current_time = 0.0; } // Animation if (current_time >= this.sync_offset) { // During/after designated playback period if (this.video_play_style[1] == DISPLAY_LOOPED) { // Currently playing this.video_tag.style.opacity = "1.0"; } else if (this.video_play_style[1] == DISPLAY_VIDEO) { // Visible this.video_tag.style.opacity = "1.0"; } else { // Image opacity this.image_tag.style.opacity = (this.video_play_style[1] == DISPLAY_NOTHING) ? 0.0 : 1.0; // State check if (current_time < this.sync_offset + this.min_duration) { // Currently playing this.video_tag.style.opacity = "1.0"; } else { // Video opacity if (this.video_fades[1]) { // Fade out var time_left = ((this.sync_offset + this.min_duration + this.video_animation_time[1]) - current_time); if (time_left > 0.0) { // Continue this_private.video_animate.call(this, 1, time_left); } else { // Completed this.video_tag.style.opacity = "0.0"; } } else { // Vanish this.video_tag.style.opacity = "0.0"; } } } } else { // Before designated playback period if (this.video_play_style[0] == DISPLAY_LOOPED) { // Currently playing this.video_tag.style.opacity = "1.0"; } else if (this.video_play_style[0] == DISPLAY_VIDEO) { // Visible this.video_tag.style.opacity = "1.0"; } else { // Image opacity this.image_tag.style.opacity = (this.video_play_style[0] == DISPLAY_NOTHING) ? 0.0 : 1.0; // Video opacity if (this.video_fades[0]) { // Fade in var wait_time = this.sync_offset - this.video_animation_time[0] - current_time; if (wait_time < 0) { // Continue this_private.video_animate.call(this, 0, this.video_animation_time[0] + wait_time); } else { // Not visible this.video_tag.style.opacity = "0.0"; // Queue for late this.video_animate_timer = setTimeout(function() { self.video_animate_timer = null; this_private.video_animate.call(self, 0, self.video_animation_time[0]); }, wait_time * 1000); } } else { // Vanish this.video_tag.style.opacity = "0.0"; } } } // Playback and sync if (current_time >= this.sync_offset) { if (this.video_play_style[1] == DISPLAY_LOOPED) { // Sync var t; if (Math.abs((t = this_private.get_video_position_at_time.call(this, current_time)) - this.video_tag.currentTime) > this.video_desync_max) { this.video_tag.currentTime = t; } // Play looped this.video_tag.loop = true; this.video_tag.play(); } else { // Remove any looping this.video_tag.loop = false; if (current_time < this.sync_offset + this.min_duration) { // Sync var t; if (Math.abs((t = this_private.get_video_position_at_time.call(this, current_time)) - this.video_tag.currentTime) > this.video_desync_max) { this.video_tag.currentTime = t; } // Play normally this.video_tag.play(); } else { // Sync at end this.video_tag.currentTime = this.video_duration; } } } else { if (this.video_play_style[0] == DISPLAY_LOOPED) { // Sync var t; if (Math.abs((t = this_private.get_video_position_at_time.call(this, current_time)) - this.video_tag.currentTime) > this.video_desync_max) { this.video_tag.currentTime = t; } // Play looped this.video_tag.loop = true; this.video_tag.play(); // Set timers to stop looping if (this.video_play_style[1] != DISPLAY_LOOPED) { this.video_loop_stop_timer = setTimeout(function() { this_private.on_timed_video_loop_stop.call(self); }, (this.sync_offset + this.min_duration - current_time) * 1000); this.video_loop_remove_timer = setTimeout(function() { this_private.on_timed_video_loop_remove.call(self); }, (this.sync_offset + this.min_duration / 2.0 - current_time) * 1000); } } else { // Sync at 0 this.video_tag.currentTime = 0.0; // Set timer to activate playback at the proper time this.sync_timer = setTimeout(function() { this_private.on_timed_video_play.call(self); }, (this.sync_offset - current_time) * 1000); } } // Play audio this.audio_tag.play(); } }, /** Internal way of seeking and synchronizing separate audio/video tracks. @param time the time to seek to */ seek_synced: function (time) { // Pause var playing = !this.paused; if (playing) { // Pause this.paused = true; this_private.clear_timers.call(this); if (this.video_tag != null) { this.video_tag.pause(); this_private.video_animate_stop.call(this); } if (this.audio_tag != null) { this.audio_tag.pause(); } } // Seek this.main_tag.currentTime = time; this_private.sync_animation_at.call(this, time); // Resume if (playing) { // Play this.paused = false; if (this.video_tag != null) { if (this.audio_tag != null) { // Play synchronized this_private.play_synced.call(this); } else { // Play only video this.video_tag.play(); } } else { // Audio only this.audio_tag.play(); } } }, /** Sets the animation of video/audio to a current time. @param time the time to sync at */ sync_animation_at: function (time) { if (this.has_both) { if (this.video_main) { this.video_tag.style.opacity = "1.0"; this.audio_tag.currentTime = this_private.get_audio_position_at_time.call(this, time); this.audio_tag.volume = this.volume * this_private.get_audio_volume_at_time.call(this, time); } else { this.video_tag.style.opacity = this_private.get_video_opacity_at_time.call(this, time); this.video_tag.currentTime = this_private.get_video_position_at_time.call(this, time); } } else { if (this.video_tag != null) this.video_tag.style.opacity = "1.0"; this.main_tag.volume = this.volume; } this.image_tag.style.opacity = this_private.get_image_opacity_at_time.call(this, time); }, /** Add a managed callback to the video tag. @param name the event name @param callback the callback function */ add_video_callback: function (name, callback) { this.video_callbacks.push([name,callback]); this.video_tag.addEventListener(name, callback); }, /** Add a managed callback to the audio tag. @param name the event name @param callback the callback function */ add_audio_callback: function (name, callback) { this.audio_callbacks.push([name,callback]); this.audio_tag.addEventListener(name, callback); }, /** Add a managed callback to the image tag. @param name the event name @param callback the callback function */ add_image_callback: function (name, callback) { this.image_callbacks.push([name,callback]); this.image_tag.addEventListener(name, callback); }, /** Get how transparent the video should be at time. Should not be called if the video is the main track. @param time the time to check @return a number between 0.0 and 1.0 representing the opacity */ get_video_opacity_at_time: function (time) { if (time >= this.sync_offset) { if (this.video_play_style[1] == DISPLAY_IMAGE || this.video_play_style[1] == DISPLAY_NOTHING) { if (time <= this.sync_offset + this.min_duration) { return 1.0; } else if (this.video_fades[1]) { var t = Math.min(this.max_duration - (this.sync_offset + this.min_duration), this.video_animation_time[1]); return Math.max(0.0, ((this.sync_offset + this.min_duration + t) - time) / t); } else { return 0.0; } } else { return 1.0; } } else { if (this.video_play_style[0] == DISPLAY_IMAGE || this.video_play_style[0] == DISPLAY_NOTHING) { if (this.video_fades[0]) { var t = Math.min(this.sync_offset, this.video_animation_time[0]); return Math.max(0.0, (time - (this.sync_offset - t)) / t); } else { return 0.0; } } else { return 1.0; } } }, /** When the audio is the main track, check where the video should be playing at given a certain time. Should not be called if the video is the main track. @param time the time to check @return the time in the video in seconds */ get_video_position_at_time: function (time) { if (time >= this.sync_offset) { if (this.video_play_style[1] == DISPLAY_LOOPED) { return (time - this.sync_offset) % this.video_duration; } else { return Math.min(this.video_duration, time - this.sync_offset); } } else { if (this.video_play_style[0] == DISPLAY_LOOPED) { return this.video_duration - ((this.sync_offset - time) % this.video_duration); } else { return 0.0; } } }, /** Get the volume of the audio at a certain time. Should not be called if the audio is the main track. @param time the time to check @return a number between 0.0 and 1.0 representing the volume factor */ get_audio_volume_at_time: function (time) { var min_time = (this.audio_play_style[0] == PLAY_LOOPED ? 0.0 : this.sync_offset); var max_time = (this.audio_play_style[1] == PLAY_LOOPED ? this.max_duration : this.sync_offset + this.min_duration); if (time >= min_time) { if (this.audio_play_style[1] == PLAY_NOTHING) { if (time <= max_time) { if (this.audio_fades[0] && time < min_time + this.audio_animation_time[0]) { return Math.min(1.0, (time - min_time) / this.audio_animation_time[0]); } else if (this.audio_fades[1] && time > max_time - this.audio_animation_time[1]) { return Math.min(1.0, (max_time - time) / this.audio_animation_time[1]); } return 1.0; } else { return 0.0; } } else { return 1.0; } } else { return (this.audio_play_style[0] == PLAY_NOTHING) ? 0.0 : 1.0; } }, /** When the video is the main track, check where the audio should be playing at given a certain time. Should not be called if the audio is the main track. @param time the time to check @return the time in the audio in seconds */ get_audio_position_at_time: function (time) { if (time >= this.sync_offset) { if (this.audio_play_style[1] == DISPLAY_LOOPED) { return (time - this.sync_offset) % this.audio_duration; } else { return Math.min(this.audio_duration, time - this.sync_offset); } } else { if (this.audio_play_style[0] == DISPLAY_LOOPED) { return this.audio_duration - ((this.sync_offset - time) % this.audio_duration); } else { return 0.0; } } }, /** Get how transparent the image should be at a given time. @param time the time to check @return a number between 0.0 and 1.0 representing the opacity */ get_image_opacity_at_time: function (time) { if (this.video_tag == null) { return 1.0; } else if (time >= this.sync_offset + this.min_duration) { return (this.video_play_style[1] == DISPLAY_NOTHING ? 0.0 : 1.0); } else { return (this.video_play_style[0] == DISPLAY_NOTHING ? 0.0 : 1.0); } }, /** Make the video fade in or out. @param mode 0 for animating in 1 for animating out other values are not valid @param time the duration of the animation in seconds */ video_animate: function (mode, time) { this_private.video_animate_stop.call(this); this.video_tag.style.opacity = (1 - mode); this.video_tag.style[this.transition_css] = "opacity " + time + "s linear"; }, /** Remove any CSS animations from the video. */ video_animate_stop: function () { this.video_tag.style.opacity = this_private.get_computed_style.call(this, this.video_tag).opacity; this.video_tag.style[this.transition_css] = ""; }, /** Get the computed style of an object. @return the computed style */ get_computed_style: function (elem) { return window.getComputedStyle(elem, null); }, /** Event callback for the video tag. Called when the metadata is ready. */ on_video_loaded_metadata: function () { // Video this.video_dimensions.width = this.video_tag.videoWidth; this.video_dimensions.height = this.video_tag.videoHeight; this.video_duration = this.video_tag.duration; this.video_tag.volume = (this.audio_tag == null ? this.volume : 0.0); if (++this.metadata_load_count == this.metadata_load_count_required) { this_private.on_metadata_ready.call(this); } }, /** Event callback for the video tag. Called when any CSS animations have completed. */ on_video_animation_end: function () { this.video_tag.style[this.transition_css] = ""; }, /** Event callback for the video tag. Called when the video ends. */ on_video_ended: function () { if (this.video_tag.loop) return; if (this.video_main) { // Pause all this.paused = true; this_private.clear_timers.call(this); if (this.audio_tag != null) { this.audio_tag.pause(); } // Event this_private.trigger.call(this, "end", { "time": this.max_duration }); } else { if (this.paused) return; // don't want this event triggering // Animation if (this.video_play_style[1] == DISPLAY_VIDEO) { // Nothing needs to be done } else { // Image opacity this.image_tag.style.opacity = (this.video_play_style[1] == DISPLAY_NOTHING) ? 0.0 : 1.0; // Video opacity if (this.video_fades[1]) { // Fade out //var t = Math.min(this.max_duration - (this.sync_offset + this.min_duration), this.video_animation_time[1]); var offset = (this.sync_offset + this.min_duration); var t = Math.min(this.max_duration - offset, this.video_animation_time[1] - (this.audio_tag.currentTime - offset)); if (t > 0) { this_private.video_animate.call(this, 1, t); } } else { // Vanish this.video_tag.style.opacity = "0.0"; } } } }, /** Event callback for the video tag. Called when the video generates an error. */ on_video_error: function () { // Event this_private.trigger.call(this, "error", { "source": "video" }); }, /** Event callback for the audio tag. Called when the metadata is ready. */ on_audio_loaded_metadata: function () { // Audio this.audio_duration = this.audio_tag.duration; this.audio_tag.volume = this.volume; if (++this.metadata_load_count == this.metadata_load_count_required) { this_private.on_metadata_ready.call(this); } }, /** Event callback for the audio tag. Called when the audio ends. */ on_audio_ended: function () { if (this.audio_tag.loop) return; if (this.video_main) { // Nothing to do } else { // Pause all this.paused = true; this_private.clear_timers.call(this); if (this.video_tag != null) { this.video_tag.pause(); this_private.video_animate_stop.call(this); } // Event this_private.trigger.call(this, "end", { "time": this.max_duration }); } }, /** Event callback for the audio tag. Called when the audio generates an error. */ on_audio_error: function () { // Event this_private.trigger.call(this, "error", { "source": "audio" }); }, /** Event callback for the video/audio tag, whichever is longer. Called when the tag generates a timeupdate event and passes it to the VPlayer listeners. */ on_main_time_update: function () { // Event this_private.trigger.call(this, "timeupdate", { "time": this.main_tag.currentTime, "duration": this.max_duration }); }, /** Event callback for the image tag. Called when the image loads. */ on_image_load: function () { this.image_dimensions.width = this.image_tag.width; this.image_dimensions.height = this.image_tag.height; this.image_tag.style.display = ""; this.image_tag.style.left = "0"; this.image_tag.style.top = "0"; this.image_tag.style.right = "0"; this.image_tag.style.bottom = "0"; this.image_tag.style.width = "100%"; this.image_tag.style.height = "100%"; if (++this.metadata_load_count == this.metadata_load_count_required) { this_private.on_metadata_ready.call(this); } }, /** Event callback for the image tag. Called when the image generates an error. */ on_image_error: function () { // Event this_private.trigger.call(this, "error", { "source": "image" }); }, /** Callback for when all metadata is loaded. Called when both of the video/audio tag metadata loaded callbacks are fired. */ on_metadata_ready: function () { // Min/max time and main track if (this.video_duration >= this.audio_duration) { this.max_duration = this.video_duration; this.min_duration = this.audio_duration; this.video_main = true; this.main_tag = this.video_tag; } else { this.max_duration = this.audio_duration; this.min_duration = this.video_duration; this.video_main = false; this.main_tag = this.audio_tag; } // Validate if (this.sync_offset + this.min_duration > this.max_duration) { this.sync_offset = this.max_duration - this.min_duration; } else if (this.sync_offset < 0.0) { this.sync_offset = 0.0; } // Animation initial state this_private.sync_animation_at.call(this, 0.0); // Time callback var self = this; if (this.video_main) { this_private.add_video_callback.call(this, "timeupdate", function () { this_private.on_main_time_update.call(self); }); } else { this_private.add_audio_callback.call(this, "timeupdate", function () { this_private.on_main_time_update.call(self); }); } // Ready this.metadata_ready = true; this_private.trigger.call(this, "load", { "video_size": this.get_video_size(), "image_size": this.get_image_size(), "duration": this.max_duration }); }, /** Synchronization timer to play video. */ on_timed_video_play: function () { this.sync_timer = null; if (this.video_play_style[0] != DISPLAY_VIDEO && !this.video_fades[0]) { // Should never be called with DISPLAY_LOOPED this.video_tag.style.opacity = "1.0"; } this.video_tag.loop = (this.video_play_style[1] == DISPLAY_LOOPED); // Play this.video_tag.play(); }, /** Timer to set the .loop attribute on the video to false. Cancels the "on_timed_video_loop_stop" timer when executed. */ on_timed_video_loop_remove: function () { // Clear timers this.video_loop_remove_timer = null; if (this.video_loop_stop_timer != null) { clearTimeout(this.video_loop_stop_timer); this.video_loop_stop_timer = null; } // Disable looping this.video_tag.loop = false; }, /** Timer to set the .loop attribute on the video to false AND stop playback. This is the fallback of the above timer, in case there are timing issues. */ on_timed_video_loop_stop: function () { // Clear timers this.video_loop_stop_timer = null; if (this.video_loop_remove_timer != null) { clearTimeout(this.video_loop_remove_timer); this.video_loop_remove_timer = null; } // Stop video this.video_tag.loop = false; this.video_tag.pause(); this.video_tag.currentTime = this.video_duration; }, /** Synchronization timer to play audio. */ on_timed_audio_play: function () { this.sync_timer = null; this.audio_tag.loop = (this.audio_play_style[1] == PLAY_LOOPED); this.audio_tag.play(); }, /** Timer to set the .loop attribute on the audio to false. Cancels the "on_timed_video_loop_stop" timer when executed. */ on_timed_audio_loop_remove: function () { // Clear timers this.audio_loop_remove_timer = null; if (this.audio_loop_stop_timer != null) { clearTimeout(this.audio_loop_stop_timer); this.audio_loop_stop_timer = null; } // Disable looping this.audio_tag.loop = false; }, /** Timer to set the .loop attribute on the audio to false AND stop playback. This is the fallback of the above timer, in case there are timing issues. */ on_timed_audio_loop_stop: function () { // Clear timers this.audio_loop_stop_timer = null; if (this.audio_loop_remove_timer != null) { clearTimeout(this.audio_loop_remove_timer); this.audio_loop_remove_timer = null; } // Stop video this.audio_tag.loop = false; this.audio_tag.pause(); this.audio_tag.currentTime = this.audio_duration; }, /** Timer to "animate" the audio tag's volume in or out. */ on_audio_animate: function () { this.audio_animate_timer = null; // Vars var self = this; var current_time = this.main_tag.currentTime; var min_time = (this.audio_play_style[0] == PLAY_LOOPED ? 0.0 : this.sync_offset); var max_time = (this.audio_play_style[1] == PLAY_LOOPED ? this.max_duration : this.sync_offset + this.min_duration); // Full if (current_time >= max_time) { this.audio_tag.volume = 0.0; // No timeout } else if (current_time >= max_time - this.audio_animation_time[1]) { this.audio_tag.volume = Math.max(0.0, (max_time - current_time) / this.audio_animation_time[1] * this.volume); // Timeout for continue this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, this.audio_animation_interval); } else if (current_time >= min_time + this.audio_animation_time[0]) { this.audio_tag.volume = this.volume; // Timeout for outro this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, ((max_time - this.audio_animation_time[1]) - current_time) * 1000); } else { this.audio_tag.volume = Math.max(0.0, (current_time - min_time) / this.audio_animation_time[0] * this.volume); // Timeout for continue this.audio_animate_timer = setTimeout(function() { this_private.on_audio_animate.call(self); }, this.audio_animation_interval); } } }; // Public methods vp.prototype = { constructor: vp, /** Generate settings from the videcode object. @param [videcode] a Videcode object which has been properly initialized if not set in the constructor, it can be set here; it is the Videcode object to use @return this */ gen_data: function (videcode) { // Clear any old data this.reset(); // Set if (videcode !== undefined) this.videcode = videcode; // On error, return if (this.videcode == null || this.videcode.has_error()) return; // Create video blob and url if (this.videcode.get_video() != null) { this.video_blob = new Blob([ this.videcode.get_video() ], {type: "video/webm"}); this.video_blob_url = (window.webkitURL || window.URL).createObjectURL(this.video_blob); ++this.metadata_load_count_required; } else { this.video_blob = null; this.video_blob_url = null; } // Create audio blob and url if (this.videcode.get_audio() != null) { this.audio_blob = new Blob([ this.videcode.get_audio() ], {type: "audio/ogg"}); this.audio_blob_url = (window.webkitURL || window.URL).createObjectURL(this.audio_blob); ++this.metadata_load_count_required; } else { this.audio_blob = null; this.audio_blob_url = null; } // Create image blob and url this.image_blob = new Blob([ this.videcode.get_image() ], {type: this.videcode.get_image_mime_type()}); this.image_blob_url = (window.webkitURL || window.URL).createObjectURL(this.image_blob); ++this.metadata_load_count_required; // Get other settings this.sync_offset = this.videcode.get_sync_offset(); this.video_fades[0] = this.videcode.get_video_fade(true); this.video_fades[1] = this.videcode.get_video_fade(false); this.audio_fades[0] = this.videcode.get_audio_fade(true); this.audio_fades[1] = this.videcode.get_audio_fade(false); this.video_play_style[0] = this.videcode.get_video_play_style(true); this.video_play_style[1] = this.videcode.get_video_play_style(false); this.audio_play_style[0] = this.videcode.get_audio_play_style(true); this.audio_play_style[1] = this.videcode.get_audio_play_style(false); return this; }, /** Remove all event listeners. @return this */ clear_listeners: function () { this.event_listeners = { "load": [], "error": [], "timeupdate": [], "volumechange": [], "seek": [], "play": [], "pause": [], "end": [], }; return this; }, /** Reset the state of the object. @return this */ reset: function () { // Remove HTML this.remove_html(); // Clear data if (this.video_blob != null) { this.video_blob = null; (window.webkitURL || window.URL).revokeObjectURL(this.video_blob_url); this.video_blob_url = null; } if (this.audio_blob != null) { this.audio_blob = null; (window.webkitURL || window.URL).revokeObjectURL(this.audio_blob_url); this.audio_blob_url = null; } if (this.image_blob != null) { this.image_blob = null; (window.webkitURL || window.URL).revokeObjectURL(this.image_blob_url); this.image_blob_url = null; } // Other settings this.volume = 0.5; this.metadata_load_count_required = 0; this.sync_offset = 0.0; this.video_fades = [ false , false ]; this.audio_fades = [ false , false ]; this.video_play_style = [ DISPLAY_NOTHING , DISPLAY_NOTHING ]; this.audio_play_style = [ PLAY_NOTHING , PLAY_NOTHING ]; return this; }, /** Check if the object has HTML generated or not. @return true if generated, false otherwise */ has_html: function () { return (this.element_container != null); }, /** Remove all the HTML elements of the object from the document. @return this */ remove_html: function () { // Clear timers this_private.clear_timers.call(this); this.pause(); // Remove HTML if (this.video_tag != null) { for (var i = 0; i < this.video_callbacks.length; ++i) { this.video_tag.removeEventListener(this.video_callbacks[i][0], this.video_callbacks[i][1]); } this.video_callbacks = []; if (this.video_tag.parentNode != null) { this.video_tag.parentNode.removeChild(this.video_tag); } this.video_tag = null; } if (this.audio_tag != null) { for (var i = 0; i < this.audio_callbacks.length; ++i) { this.audio_tag.removeEventListener(this.audio_callbacks[i][0], this.audio_callbacks[i][1]); } this.audio_callbacks = []; if (this.audio_tag.parentNode != null) { this.audio_tag.parentNode.removeChild(this.audio_tag); } this.audio_tag = null; } if (this.image_tag != null) { for (var i = 0; i < this.image_callbacks.length; ++i) { this.image_tag.removeEventListener(this.image_callbacks[i][0], this.image_callbacks[i][1]); } this.image_callbacks = []; if (this.image_tag.parentNode != null) { this.image_tag.parentNode.removeChild(this.image_tag); } this.image_tag = null; } if (this.element_container != null) { if (this.element_container.parentNode != null) { this.element_container.parentNode.removeChild(this.element_container); } this.element_container = null; } this.main_tag = { "currentTime": 0.0 }; // have defaults so get_time() can work without fail // Other settings this.paused = true; this.video_duration = 0.0; this.audio_duration = 0.0; this.max_duration = 0.0; this.min_duration = 0.0; this.video_dimensions = { width: 0, height: 0 }; this.image_dimensions = { width: 0, height: 0 }; this.metadata_load_count = 0; this.metadata_ready = false; this.video_main = true; this.has_both = false; return this; }, /** Create the HTML elements for the player. The new components will be added in a new div tag into the specified container. The components include a video tag (if there is video), an audio tag (if there is audio), and an img tag. The div tag will fill the nearest relative container, as it is positioned absolutely. Events should generally be hooked before calling this. @param container the container to add the new elements to @return this */ create_html: function (container) { if (this.image_blob == null || (this.audio_blob == null && this.video_blob == null) || this.element_container != null) return this; var self = this; // Create container this.element_container = document.createElement("div"); this.element_container.style.position = "relative"; this.element_container.style.display = "inline-block"; if (container != null) container.appendChild(this.element_container); // Create image this.image_tag = document.createElement("img"); this.image_tag.style.position = "absolute"; this.image_tag.style.display = "none"; this.image_tag.style.margin = "0px"; this.image_tag.style.padding = "0px"; this.image_tag.style.border = "0px hidden"; this.image_tag.style["float"] = "none"; // Image events this_private.add_image_callback.call(this, "load", function () { this_private.on_image_load.call(self); }); this_private.add_image_callback.call(this, "error", function () { this_private.on_image_error.call(self); }); // Load this.image_tag.setAttribute("src", this.image_blob_url); this.element_container.appendChild(this.image_tag); // Create video if (this.video_blob_url != null) { this.video_tag = document.createElement("video"); this.video_tag.style.position = "absolute"; this.video_tag.style.left = "0"; this.video_tag.style.top = "0"; this.video_tag.style.right = "0"; this.video_tag.style.bottom = "0"; this.video_tag.style.width = "100%"; this.video_tag.style.height = "100%"; this.video_tag.style.opacity = "0.0"; // Video events this_private.add_video_callback.call(this, "loadedmetadata", function () { this_private.on_video_loaded_metadata.call(self); }); this_private.add_video_callback.call(this, "ended", function () { this_private.on_video_ended.call(self); }); this_private.add_video_callback.call(this, "error", function () { this_private.on_video_error.call(self); }); this_private.add_video_callback.call(this, this.transition_end_event_name, function () { this_private.on_video_animation_end.call(self); }); // Load video this.video_tag.setAttribute("src", this.video_blob_url); this.element_container.appendChild(this.video_tag); } // Create audio if (this.audio_blob_url != null) { this.audio_tag = document.createElement("audio"); this.audio_tag.style.display = "none"; // Audio events this_private.add_audio_callback.call(this, "loadedmetadata", function () { this_private.on_audio_loaded_metadata.call(self); }); this_private.add_audio_callback.call(this, "ended", function () { this_private.on_audio_ended.call(self); }); this_private.add_audio_callback.call(this, "error", function () { this_private.on_audio_error.call(self); }); // Load audio this.audio_tag.setAttribute("src", this.audio_blob_url); this.element_container.appendChild(this.audio_tag); } // Both/main tag if (this.video_tag != null) { if (this.audio_tag != null) { this.has_both = true; this.main_tag = (this.video_main ? this.video_tag : this.audio_tag); } else { this.has_both = false; this.main_tag = this.video_tag; } } else { this.has_both = false; this.main_tag = this.audio_tag; } // Done return this; }, /** Get the HTML div container element created in the create_html() method. @return null if not generated yet, otherwise a HTML div element */ get_container: function () { return this.element_container; }, /** Get the generated image URL. @return the blob URL */ get_image: function () { return this.image_blob_url; }, /** Get the generated video URL. @return the blob URL, or null if no video */ get_video: function () { return this.video_blob_url; }, /** Get the generated audio URL. @return the blob URL, or null if no audio */ get_audio: function () { return this.audio_blob_url; }, /** Get the current volume level. @return a value between 0.0 and 1.0, 1.0 being the max */ get_volume: function () { return this.volume; }, /** Set the current volume level. This method can be called at any time. @param volume a number between 0.0 and 1.0, 1.0 being the max */ set_volume: function (volume) { if (volume < 0.0) volume = 0.0; else if (volume > 1.0) volume = 1.0; this.volume = volume; if (this.has_both) { if (this.video_main) { this.audio_tag.volume = this.volume * this_private.get_audio_volume_at_time.call(this, this.main_tag.currentTime); } else { this.audio_tag.volume = this.volume; } } else { this.main_tag.volume = this.volume; } // Event this_private.trigger.call(this, "volumechange", { "volume": this.volume }); }, /** Play the player. Will do nothing if the object isn't ready or is already playing. */ play: function () { if (!this.paused || !this.metadata_ready) return; // Play this.paused = false; if (this.has_both) { // Sync'd this_private.play_synced.call(this); } else { // Video/audio only this.main_tag.play(); } // Event this_private.trigger.call(this, "play", { "time": this.main_tag.currentTime }); }, /** Pause the player. Will do nothing if the object isn't ready or is already paused. */ pause: function () { if (this.paused || !this.metadata_ready) return; // Pause all this.paused = true; this_private.clear_timers.call(this); if (this.video_tag != null) { this.video_tag.pause(); this_private.video_animate_stop.call(this); } if (this.audio_tag != null) { this.audio_tag.pause(); } // Event this_private.trigger.call(this, "pause", { "time": this.main_tag.currentTime }); }, /** Seek to a specific time. @param time a value between 0.0 and get_duration() */ seek: function (time) { if (!this.metadata_ready) return; // Limit time if (time < 0.0) time = 0.0; else if (time > this.max_duration) time = this.max_duration; // Seek if (this.has_both) { // Play synchronized this_private.seek_synced.call(this, time); } else { // Video/audio only this.main_tag.currentTime = time; } // Event this_private.trigger.call(this, "seek", { "time": time, "duration": this.max_duration }); }, /** Check if the player is playing anything or not. @return true if playing, false if paused/not playing */ is_paused: function () { return this.paused; }, /** Add an event callback in a jQuery-esque style. @param event_name the name of the event @param callback the function callback, in the form of: function (data) @return this */ on: function (event_name, callback) { if (typeof(callback) != function_type) return this; // Event adding if (event_name in this.event_listeners) { this.event_listeners[event_name].push(callback); } // Done return this; }, /** Remove an event callback in a jQuery-esque style. @param event_name the name of the event @param [callback] if omitted, removes all callbacks on the event, otherwise, it is the function to remove @return this */ off: function (event_name, callback) { if (event_name in this.event_listeners) { if (typeof(callback) == function_type) { // Remove single var list = this.event_listeners[event_name]; for (var i = 0; i < list.length; ++i) { if (list[i] === callback) { list.splice(i, 1); break; } } } else { // Remove all this.event_listeners[event_name] = []; } } // Done return this; }, /** Get the size of the video. If there is no video, the values are both 0. @return an object in the form of { width:? , height:? } */ get_video_size: function () { return { "width": this.video_dimensions.width, "height": this.video_dimensions.height }; }, /** Get the size of the image. @return an object in the form of { width:? , height:? } */ get_image_size: function () { return { "width": this.image_dimensions.width, "height": this.image_dimensions.height }; }, /** Get the full duration of the object, in seconds. @return the duration */ get_duration: function () { return this.max_duration; }, /** Get the minimum duration of the object; that is, if there is both audio and video in separate tags, returns the minimum length of the two. Otherwise, it returns the same as get_duration(). @return the minimum duration */ get_min_duration: function () { return (this.has_both ? this.min_duration : this.max_duration); }, /** Get the current time of the player. @return the time in seconds */ get_time: function () { return this.main_tag.currentTime; }, /** Get the blob URL of the video object. @return a string URL, or null if there is no video */ get_video_url: function () { return this.video_blob_url; }, /** Get the blob URL of the audio object. @return a string URL, or null if there is no separate audio */ get_audio_url: function () { return this.audio_blob_url; }, /** Get the blob URL of the image object. @return a string URL */ get_image_url: function () { return this.image_blob_url; }, /** Check if the video is the main tag; that is, if there is a video and audio tag, the video is longer than the audio. This is meaningless to call if there aren't both video and audio. @return true if the video is longer, false otherwise */ is_video_main: function () { return this.video_main; }, /** Returns the synchronization offset. This value should always be in a valid range. @return a number in seconds of the offset */ get_sync_offset: function () { return this.sync_offset; }, /** Check if the object has a video tag. @return true if it has video, false otherwise */ has_video: function () { return (this.video_tag != null); }, /** Check if the object has a audio tag. @return true if it has audio, false otherwise */ has_audio: function () { return (this.audio_tag != null); }, /** Check if the object has both audio and video tags. @return true if it has both, false otherwise */ has_video_and_audio: function () { return this.has_both; } }; // Return return vp; })();