diff --git a/examples/inspector/js/inspectorGfx.js b/examples/inspector/js/inspectorGfx.js index 7d6b34ad87..b1a21e01c2 100644 --- a/examples/inspector/js/inspectorGfx.js +++ b/examples/inspector/js/inspectorGfx.js @@ -24,6 +24,7 @@ function createPlaybackEaselHost(file) { var easel; function createEasel() { + Shumway.GFX.VP6Player.SWFPath = "../../utils/vp6player/vp6player.swf"; Shumway.GFX.WebGL.SHADER_ROOT = "../../src/gfx/gl/shaders/"; easel = new Easel(document.getElementById("easelContainer")); easel.startRendering(); diff --git a/extension/firefox/Makefile b/extension/firefox/Makefile index 0396b92ff2..2dac5153f0 100644 --- a/extension/firefox/Makefile +++ b/extension/firefox/Makefile @@ -42,6 +42,7 @@ build: ensureoutputdir mkdir -p $(BUILD_DIR)/content/libs cp ../../build/libs/builtin.abc $(BUILD_DIR)/content/libs/ cp ../../build/libs/relooper.js $(BUILD_DIR)/content/libs/ + cp ../../utils/vp6player/vp6player.swf $(BUILD_DIR)/content/ # Copying closure optimized shumway.js files cp $(BUNDLES_DIR)/shumway*.js $(BUILD_DIR)/content/ cp ../../build/version/version.txt $(BUILD_DIR)/content/version.txt @@ -71,6 +72,7 @@ restartless: ln -s ../../../build/libs $(RESTARTLESS_DIR)/content/libs ln -s ../../../build/bundles/shumway.gfx.js $(RESTARTLESS_DIR)/content/shumway.gfx.js ln -s ../../../build/bundles/shumway.player.js $(RESTARTLESS_DIR)/content/shumway.player.js + ln -s ../../../utils/vp6player/vp6player.swf $(RESTARTLESS_DIR)/content/vp6player.swf ln -s ../../../build/version/version.txt $(RESTARTLESS_DIR)/content/version.txt cd $(RESTARTLESS_DIR); pwd > "$(PROFILE)/extensions/shumway@research.mozilla.org" grep "nglayout.debug.disable_xul_cache\", true" "$(PROFILE)/prefs.js" 1>/dev/null || echo "Set create 'nglayout.debug.disable_xul_cache' boolean preference to 'true'." diff --git a/extension/firefox/content/ShumwayStreamConverter.jsm b/extension/firefox/content/ShumwayStreamConverter.jsm index 58c910f5e0..b43ad7a4e4 100644 --- a/extension/firefox/content/ShumwayStreamConverter.jsm +++ b/extension/firefox/content/ShumwayStreamConverter.jsm @@ -106,6 +106,11 @@ function isShumwayEnabledFor(startupInfo) { var url = startupInfo.url; var baseUrl = startupInfo.baseUrl; + if (/^resource:\/\/shumway\//i.test(url)) { + // disabled for internal Shumway SWFs + return false; + } + // blacklisting well known sites with issues if (/\.ytimg\.com\//i.test(url) /* youtube movies */ || /\/vui.swf\b/i.test(url) /* vidyo manager */ || diff --git a/src/base/remoting.ts b/src/base/remoting.ts index 3c95a95798..ab45831494 100644 --- a/src/base/remoting.ts +++ b/src/base/remoting.ts @@ -219,6 +219,7 @@ module Shumway.Remoting { registerImage(syncId: number, symbolId: number, imageType: ImageType, data: Uint8Array, alphaData: Uint8Array): Promise; fscommand(command: string, args: string): void; + sendVP6StreamData(url: string, data: Uint8Array): void; } /** diff --git a/src/base/utilities.ts b/src/base/utilities.ts index 093e4f1572..5d7f35c287 100644 --- a/src/base/utilities.ts +++ b/src/base/utilities.ts @@ -1471,6 +1471,32 @@ module Shumway { return Random.next(); }; + function formatUint16(n): string { + var s = n.toString(16); + switch (n.length) { + case 1: + return '000' + s; + case 2: + return '00' + s; + case 3: + return '0' + s; + default: + return s; + } + } + + export function generateRandomUUID(): string { + var data = new Uint16Array(8); + (window).crypto.getRandomValues(data); + // See rfc 4122, section 4.4 + data[3] = (data[3] & 0x0FFF) | 0x4000; // time_hi_and_version bits 12-15 set to 0100 + data[4] = (data[4] & 0x3FFF) | 0x8000; // clock_seq_hi_and_reserved bits 6-7 to 10 + return formatUint16(data[0]) + formatUint16(data[1]) + '-' + + formatUint16(data[2]) + '-' + formatUint16(data[3]) + '-' + + formatUint16(data[4]) + '-' + formatUint16(data[5]) + + formatUint16(data[6]) + formatUint16(data[7]); + } + function polyfillWeakMap() { if (typeof jsGlobal.WeakMap === 'function') { return; // weak map is supported diff --git a/src/flash/net/NetStream.ts b/src/flash/net/NetStream.ts index 4f41edc75a..e2e39c8088 100644 --- a/src/flash/net/NetStream.ts +++ b/src/flash/net/NetStream.ts @@ -162,10 +162,6 @@ module Shumway.AVMX.AS.flash.net { notImplemented("public flash.net.NetStream::dispose"); return; } - _getVideoStreamURL(): string { - return this._videoStream.url; - } - play(url: string): void { flash.media.SoundMixer._registerSoundSource(this); @@ -183,8 +179,13 @@ module Shumway.AVMX.AS.flash.net { this._videoStream.play(url, this.checkPolicyFile); } - this._notifyVideoControl(VideoControlEvent.Init, { - url: this._videoStream.url + this._videoStream.urlPromise.then((url: string) => { + this._notifyVideoControl(VideoControlEvent.Init, { + url: url + }); + }, (reason: any) => { + this.dispatchEvent(new this.sec.flash.events.NetStatusEvent(events.NetStatusEvent.NET_STATUS, + false, false, this.sec.createObjectFromJS({code: "NetStream.Play.NoSupportedTrackFound", level: "error"}))); }); } play2(param: flash.net.NetStreamPlayOptions): void { @@ -588,11 +589,10 @@ module Shumway.AVMX.AS.flash.net { private _started: boolean; private _buffer: string; private _bufferTime: number; - private _url: string; + private _urlPromise: PromiseWrapper; private _contentTypeHint: string; private _state: VideoStreamState; private _mediaSource; - private _mediaSourceBuffer; private _mediaSourceBufferLock: Promise; private _head: Uint8Array; private _decoder: IDataDecoder; @@ -617,9 +617,8 @@ module Shumway.AVMX.AS.flash.net { this._started = false; this._buffer = 'empty'; this._bufferTime = 0.1; - this._url = null; + this._urlPromise = new PromiseWrapper(); this._mediaSource = null; - this._mediaSourceBuffer = null; this._mediaSourceBufferLock = null; this._contentTypeHint = null; this._state = VideoStreamState.CLOSED; @@ -627,8 +626,8 @@ module Shumway.AVMX.AS.flash.net { this._netStream = netStream; } - get url(): string { - return this._url; + get urlPromise(): Promise { + return this._urlPromise.promise; } play(url: string, checkPolicyFile: boolean) { @@ -639,24 +638,32 @@ module Shumway.AVMX.AS.flash.net { Debug.warning('MediaSource API is not enabled, falling back to regular playback'); isMediaSourceEnabled = false; } + var flvMode: string = flvOption.value; var forceMediaSource = false; - if (/\.flv($|\?)/i.test(url)) { - if (flvOption.value === 'supported') { + var useVP6Player = false; + if (vp6PlayerOption.value) { + useVP6Player = true; + } else if (/\.flv($|\?)/i.test(url)) { + if (flvMode === 'supported') { forceMediaSource = true; - } else if (flvOption.value === 'mock') { + } else if (flvMode === 'mock') { url = 'resource://shumway/web/noflv.mp4'; + } else if (flvMode === 'flash') { + useVP6Player = true; } else { - setTimeout(() => { - this._netStream.dispatchEvent(new this.sec.flash.events.NetStatusEvent(events.NetStatusEvent.NET_STATUS, - false, false, this.sec.createObjectFromJS({code: "NetStream.Play.NoSupportedTrackFound", level: "error"}))); - }); + this._urlPromise.reject('Not supported'); return; } } + if (!forceMediaSource && !isMediaSourceEnabled) { somewhatImplemented("public flash.net.NetStream::play"); this._state = VideoStreamState.OPENED; - this._url = FileLoadingService.instance.resolveUrl(url); + url = FileLoadingService.instance.resolveUrl(url); + if (useVP6Player) { + url = 'vp6:' + url; + } + this._urlPromise.resolve(url); return; } @@ -741,27 +748,55 @@ module Shumway.AVMX.AS.flash.net { openInDataGenerationMode() { release || assert(this._state === VideoStreamState.CLOSED); this._state = VideoStreamState.OPENED_DATA_GENERATION; + } + + private _createMediaSource(contentType: string): Promise { + var mediaSourceReady = new PromiseWrapper(); var mediaSource = new MediaSource(); mediaSource.addEventListener('sourceopen', function(e) { this._ensurePlaying(); + var mediaSourceBuffer = this._mediaSource.addSourceBuffer(contentType); + mediaSourceReady.resolve(mediaSourceBuffer); }.bind(this)); mediaSource.addEventListener('sourceend', function(e) { this._mediaSource = null; }.bind(this)); this._mediaSource = mediaSource; - this._url = URL.createObjectURL(mediaSource); + this._mediaSourceBufferLock = mediaSourceReady.promise; + var url = URL.createObjectURL(mediaSource); + this._urlPromise.resolve(url); + return mediaSourceReady.promise; + } + + private _createVP6PlayerDecoder(): IDataDecoder { + var url = 'vp6:flvstream:' + generateRandomUUID(); + this._urlPromise.resolve(url); + var player: any = this.sec.player; + // Shortcut: using decoder interface to push data to the gfx iframe. + // FIXME don't use player._gfxService -- implement interface + return { + onData: null, + onError: null, + push: function (bytes: Uint8Array) { player.sendVP6StreamData(url, bytes); }, + close: function () { player.sendVP6StreamData(url, null /* EOF */); } + }; } appendBytes(bytes: Uint8Array) { release || assert(this._state === VideoStreamState.OPENED_DATA_GENERATION || this._state === VideoStreamState.OPENED); - release || assert(this._mediaSource); if (this._decoder) { this._decoder.push(bytes); return; } + if (vp6PlayerOption.value) { + this._decoder = this._createVP6PlayerDecoder(); + this._decoder.push(bytes); + return; + } + // First we need to parse some content to find out mime type and codecs // for MediaSource. Caching some data at the beginning until we can tell // the type of the content. @@ -783,21 +818,19 @@ module Shumway.AVMX.AS.flash.net { // FLV data needs to be parsed and wrapped with MP4 tags. var flvDecoder = new FlvMp4Decoder(this.sec); flvDecoder.onHeader = function (contentType) { - this._mediaSourceBuffer = this._mediaSource.addSourceBuffer(contentType); - this._mediaSourceBufferLock = Promise.resolve(undefined); + this._createMediaSource(contentType); }.bind(this); flvDecoder.onData = this._queueData.bind(this); this._decoder = flvDecoder; } else if (contentType) { - // Let's use identity decoder for reset of the types. + // Let's use identity decoder for rest of the types. this._decoder = { onData: this._queueData.bind(this), onError: function (e) { /* */ }, push: function (bytes: Uint8Array) { this.onData(bytes); }, close: function () { /* */ } }; - this._mediaSourceBuffer = this._mediaSource.addSourceBuffer(contentType); - this._mediaSourceBufferLock = Promise.resolve(undefined); + this._createMediaSource(contentType); } } @@ -819,13 +852,12 @@ module Shumway.AVMX.AS.flash.net { private _queueData(bytes: Uint8Array): void { // We need to chain all appendBuffer operations using 'update' event. - var buffer = this._mediaSourceBuffer; - this._mediaSourceBufferLock = this._mediaSourceBufferLock.then(function () { + this._mediaSourceBufferLock = this._mediaSourceBufferLock.then(function (buffer) { buffer.appendBuffer(bytes); return new Promise(function (resolve) { buffer.addEventListener('update', function updateHandler() { buffer.removeEventListener('update', updateHandler); - resolve(); + resolve(buffer); }); }); }); @@ -842,12 +874,14 @@ module Shumway.AVMX.AS.flash.net { throw new Error('Internal appendBytes error'); } this._decoder.close(); - this._mediaSourceBufferLock.then(function () { - if (this._mediaSource) { - this._mediaSource.endOfStream(); - } - this.close(); - }.bind(this)); + if (this._mediaSourceBufferLock) { + this._mediaSourceBufferLock.then(function (buffer) { + if (this._mediaSource) { + this._mediaSource.endOfStream(); + } + this.close(); + }.bind(this)); + } } somewhatImplemented("public flash.net.NetStream::appendBytesAction"); } diff --git a/src/flash/settings.ts b/src/flash/settings.ts index 83007554d7..a54c407d9b 100644 --- a/src/flash/settings.ts +++ b/src/flash/settings.ts @@ -30,6 +30,10 @@ module Shumway.AVMX.AS { new Shumway.Options.Option(null, "Use Media Source for Video", "boolean", false, "Enables Media Source Extension API for NetStream.") ); + export var vp6PlayerOption = flashOptions.register ( + new Shumway.Options.Option(null, "Use VP6Player for Video", "boolean", false, "Always use VP6Player for media playback.") + ); + export var mediaSourceMP3Option = flashOptions.register ( new Shumway.Options.Option(null, "Use Media Source for MP3", "boolean", true, "Enables Media Source Extension API for MP3 streams.") ); diff --git a/src/gfx/easelHost.ts b/src/gfx/easelHost.ts index 14fb3f5d7f..a094ea9ead 100644 --- a/src/gfx/easelHost.ts +++ b/src/gfx/easelHost.ts @@ -185,6 +185,10 @@ module Shumway.GFX { arguments.length; // keeping from closure removal } + processVP6StreamData(url: string, data: Uint8Array) { + VP6Player.addVP6StreamData(url, data); + } + processFrame() { arguments.length; // keeping from closure removal } diff --git a/src/gfx/references-base.ts b/src/gfx/references-base.ts index 739a4f84c7..01f5a7664a 100644 --- a/src/gfx/references-base.ts +++ b/src/gfx/references-base.ts @@ -23,6 +23,7 @@ /// /// /// +/// /// /// diff --git a/src/gfx/renderables/renderables.ts b/src/gfx/renderables/renderables.ts index 50a661fa52..c137c8e6aa 100644 --- a/src/gfx/renderables/renderables.ts +++ b/src/gfx/renderables/renderables.ts @@ -212,7 +212,23 @@ module Shumway.GFX { this._assetId = assetId; this._eventSerializer = eventSerializer; - var element: HTMLVideoElement = document.createElement('video'); + RenderableVideo._renderableVideos.push(this); + + if (typeof registerInspectorAsset !== "undefined") { + registerInspectorAsset(-1, -1, this); + } + + this._state = RenderableVideoState.Idle; + } + + private _setupVideoElement(useFlash: boolean): HTMLVideoElement { + var element: HTMLVideoElement; + if (useFlash) { + element = VP6Player.createDOMElement(); + } else { + element = document.createElement('video'); + } + var elementEventHandler = this._handleVideoEvent.bind(this); element.preload = 'metadata'; // for mobile devices element.addEventListener("play", elementEventHandler); @@ -230,14 +246,7 @@ module Shumway.GFX { this._video = element; this._videoEventHandler = elementEventHandler; - - RenderableVideo._renderableVideos.push(this); - - if (typeof registerInspectorAsset !== "undefined") { - registerInspectorAsset(-1, -1, this); - } - - this._state = RenderableVideoState.Idle; + return element; } public get video(): HTMLVideoElement { @@ -342,7 +351,9 @@ module Shumway.GFX { var ESTIMATED_VIDEO_SECOND_SIZE: number = 500; switch (type) { case VideoControlEvent.Init: - videoElement.src = data.url; + var useVP6Player = data.url && data.url.indexOf('vp6:') === 0; + videoElement = this._setupVideoElement(useVP6Player); + videoElement.src = useVP6Player ? data.url.substring('vp6:'.length) : data.url; this.play(); this._notifyNetStream(VideoPlaybackEvent.Initialized, null); break; @@ -405,6 +416,9 @@ module Shumway.GFX { } public checkForUpdate() { + if (!this._video) { + return; + } if (this._lastTimeInvalidated !== this._video.currentTime) { // Videos composited using DOM elements don't need to invalidate parents. if (!this._isDOMElement) { @@ -422,10 +436,13 @@ module Shumway.GFX { if (renderable.willRender()) { // If the nodes video element isn't already on the video layer, mark the node as invalid to // make sure the video element will be added the next time the renderer reaches it. - if (!renderable._video.parentElement) { + if (!renderable._video || !renderable._video.parentElement || + renderable._video.parentElement === VP6Player.hiddenLayer) { renderable.invalidate(); } - renderable._video.style.zIndex = renderable.parents[0].depth + ''; + if (renderable._video) { + renderable._video.style.zIndex = renderable.parents[0].depth + ''; + } } else if (renderable._video.parentElement) { // The nodes video element should be removed if no longer visible. renderable._dispatchEvent(NodeEventType.RemovedFromStage); diff --git a/src/gfx/vp6player.ts b/src/gfx/vp6player.ts new file mode 100644 index 0000000000..b29b574965 --- /dev/null +++ b/src/gfx/vp6player.ts @@ -0,0 +1,309 @@ +/** + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module Shumway.GFX { + + var objectID = 0; + function generateUniqueID(): string { + return '_vp6player' + (objectID++); + } + + var hiddenVP6PlayersLayer: HTMLElement; + function getHiddenVP6PlayersLayer() { + if (!hiddenVP6PlayersLayer) { + hiddenVP6PlayersLayer = document.createElement('div'); + hiddenVP6PlayersLayer.setAttribute('style', 'position:absolute;width:0;height:0;overflow:hidden'); + document.body.appendChild(hiddenVP6PlayersLayer); + } + return hiddenVP6PlayersLayer; + } + + function packageAndSendVP6Data(obj: HTMLObjectElement, data: Uint8Array): void { + if (data == null) { + (obj).addVP6Data(null); + return; + } + var maxSize = 1023; // divisible by 3 + var pos = 0; + while (pos + maxSize < data.length) { + var chunk = btoa(String.fromCharCode.apply(null, data.subarray(pos, pos + maxSize))); + (obj).addVP6Data(chunk); + pos += maxSize; + } + var chunk = btoa(String.fromCharCode.apply(null, data.subarray(pos))); + (obj).addVP6Data(chunk); + } + + export class VP6Player { + public static SWFPath: string = 'resource://shumway/vp6player.swf'; + + private _obj: HTMLObjectElement | any; + private _shadowObj: any; + private _initialized: boolean; + private _playing: boolean; + private _errorObj: {code: number}; + private _seekableObj: {length: number}; + private _bufferedObj: {length: number; end: (index: number) => number}; + private _vp6Data: Uint8Array[]; + + public constructor() { + var id = generateUniqueID(); + var proxy = this; + var obj = document.createElement('object'), param; + obj.id = id; + obj.setAttribute('type', 'application/x-shockwave-flash'); + obj.setAttribute('data', VP6Player.SWFPath); + // FIXME we still need to add file:/// path to vp6player.swf in the profile to the trusted Flash locations + param = document.createElement('param'); + param.setAttribute('name', 'allowScriptAccess'); + param.setAttribute('value', 'always'); + obj.appendChild(param); + param = document.createElement('param'); + param.setAttribute('name', 'scale'); + param.setAttribute('value', 'exactfit'); + obj.appendChild(param); + param = document.createElement('param'); + param.setAttribute('name', 'flashvars'); + param.setAttribute('value', 'id=' + id); + obj.appendChild(param); + param = document.createElement('param'); + param.setAttribute('name', 'shumwaymode'); + param.setAttribute('value', 'off'); + obj.appendChild(param); + + this._obj = obj; + this._shadowObj = Object.create(null); + this._initialized = false; + this._playing = false; + + this._errorObj = { + get code() { + return proxy._shadowObj.errorCode; + } + }; + this._seekableObj = { + get length() { + return proxy._initialized ? 1 : 0; + } + }; + this._bufferedObj = { + get length() { + return proxy._initialized ? 1 : 0; + }, + end: function (index) { + return this._obj._getBufferedTime(); + } + }; + + this._obj._proxy = this; + } + + public static createDOMElement(): HTMLElement { + var div = document.createElement('div'); + div.style.display = 'inline-block'; + VP6Player.call(div); + var propertiesToTransfer = ['width', 'height', 'src', 'currentTime', + 'volume', 'error', 'videoWidth', 'videoHeight', 'paused', 'seekable', + 'buffered', 'play', 'pause', '_dispatchEvent', '_pushProperty', + '_syncProperties', '_init', '_setSize', '_addVP6StreamData']; + propertiesToTransfer.forEach((x) => { + var desc = Object.getOwnPropertyDescriptor(VP6Player.prototype, x); + Debug.assert(desc); + Object.defineProperty(div, x, desc); + }); + div.appendChild((div)._obj); + getHiddenVP6PlayersLayer().appendChild(div); + return div; + } + + public get width() { + return this._obj.width; + } + + public set width(value) { + this._obj.width = value; + } + + public get height() { + return this._obj.height; + } + + public set height(value) { + this._obj.height = value; + } + + public get src(): string { + return this._shadowObj.src; + } + + public set src(value: string) { + this._obj.setAttribute('data-src', 'vp6:' + value); + this._shadowObj.src = value; + this._pushProperty('src'); + } + + public get currentTime(): number { + if (!this._initialized) { + return NaN; + } + return this._obj.getTime(); + } + + public set currentTime(value: number) { + this._obj.setTime(value); + } + + public get volume(): number { + return this._shadowObj.volume; + } + + public set volume(value: number) { + this._shadowObj.volume = value; + this._pushProperty('volume'); + } + + public get error() { + return this._errorObj; + } + + public get videoWidth(): number { + return this._shadowObj.videoWidth; + } + + public get videoHeight(): number { + return this._shadowObj.videoHeight; + } + + public get duration(): number { + return this._shadowObj.duration; + } + + public get paused(): boolean { + return this._shadowObj.paused; + } + + public get seekable() { + return this._seekableObj; + } + + public get buffered() { + return this._bufferedObj; + } + + public play(): void { + if (!this._initialized) { + this._playing = true; + return; + } + this._obj.play(); + } + + public pause(): void { + if (!this._initialized) { + this._playing = false; + return; + } + this._obj.pause(); + } + + _dispatchEvent(eventName) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent(eventName, false, false, null); + this._obj.parentElement.dispatchEvent(event); + } + + _pushProperty(propName) { + if (this._initialized) { + this._obj.setProperty(propName, this._shadowObj[propName]); + } + } + + _syncProperties(props) { + for (var i in props) { + this._shadowObj[i] = props[i]; + } + } + + _init() { + this._initialized = true; + if (this._shadowObj.src !== undefined) { + this._pushProperty('src'); + } + if (this._shadowObj.volume !== undefined) { + this._pushProperty('volume'); + } + if (this._vp6Data) { + this._vp6Data.forEach((data) => { + packageAndSendVP6Data(this._obj, data); + }); + this._vp6Data = null; + } + if (this._playing) { + this._obj.play(); + } + } + + _setSize(width: number, height: number) { + if (!this._obj.width) { + this._obj.width = width; + } + if (!this._obj.height) { + this._obj.height = height; + } + } + + _addVP6StreamData(data: Uint8Array) { + if (this._initialized) { + packageAndSendVP6Data(this._obj, data); + } else { + if (!this._vp6Data) { + this._vp6Data = []; + } + this._vp6Data.push(data); + } + } + + static __dispatchEvent(id: string, eventName: string) { + var target = document.getElementById(id); + target._proxy._dispatchEvent(eventName); + } + + static __syncProperties(id: string, props: any) { + var target = document.getElementById(id); + target._proxy._syncProperties(props); + } + + static __init(id: string) { + var target = document.getElementById(id); + target._proxy._init(); + } + + static __setSize(id: string, width: number, height: number) { + var target = document.getElementById(id); + target._proxy._setSize(width, height); + } + + public static get hiddenLayer() { + return getHiddenVP6PlayersLayer(); + } + + public static addVP6StreamData(url: string, data: Uint8Array) { + var target = document.querySelector('object[data-src="' + url + '"]'); + target._proxy._addVP6StreamData(data); + } + } +} + diff --git a/src/gfx/window/windowEaselHost.ts b/src/gfx/window/windowEaselHost.ts index d48f115319..1a3e71d641 100644 --- a/src/gfx/window/windowEaselHost.ts +++ b/src/gfx/window/windowEaselHost.ts @@ -103,6 +103,8 @@ module Shumway.GFX.Window { this._sendRegisterImageResponse.bind(this, data.requestId)); } else if (data.type === 'fscommand') { this.processFSCommand(data.command, data.args); + } else if (data.type === 'vp6data') { + this.processVP6StreamData(data.url, data.data); } else { // release || Debug.assertUnreachable("Unhandled remoting event " + data.type); } diff --git a/src/player/player.ts b/src/player/player.ts index 6f41260230..56254d0c5c 100644 --- a/src/player/player.ts +++ b/src/player/player.ts @@ -107,6 +107,10 @@ module Shumway.Player { throw new Error('This method is abstract'); } + sendVP6StreamData(url: string, data: Uint8Array): void { + throw new Error('This method is abstract'); + } + public processUpdates(updates: DataBuffer, assets: any []) { var deserializer = new Remoting.Player.PlayerChannelDeserializer( this.sec, updates, assets); @@ -499,6 +503,10 @@ module Shumway.Player { this._gfxService.fscommand(command, args); } + public sendVP6StreamData(url: string, data: Uint8Array): void { + this._gfxService.sendVP6StreamData(url, data); + } + public requestRendering(): void { this._pumpDisplayListUpdates(); } diff --git a/src/player/window/windowPlayer.ts b/src/player/window/windowPlayer.ts index 22f0cf7b6c..13a31928bf 100644 --- a/src/player/window/windowPlayer.ts +++ b/src/player/window/windowPlayer.ts @@ -117,6 +117,14 @@ module Shumway.Player.Window { }); } + sendVP6StreamData(url: string, data: Uint8Array): void { + this._peer.postAsyncMessage({ + type: 'vp6data', + url: url, + data: data + }); + } + private onWindowMessage(data) { if (typeof data === 'object' && data !== null) { switch (data.type) { diff --git a/src/shell/shell.ts b/src/shell/shell.ts index 6dd8baa2d0..0a350818a9 100644 --- a/src/shell/shell.ts +++ b/src/shell/shell.ts @@ -178,6 +178,10 @@ module Shumway.Shell { microTaskQueue.stop(); } } + + sendVP6StreamData(url: string, data: Uint8Array): void { + // console.log('vp6PlayerData'); + } } export var verbose = false; diff --git a/utils/vp6player/org/mozilla/vp6player/VP6Player.as b/utils/vp6player/org/mozilla/vp6player/VP6Player.as new file mode 100644 index 0000000000..c219c6bf83 --- /dev/null +++ b/utils/vp6player/org/mozilla/vp6player/VP6Player.as @@ -0,0 +1,257 @@ +package org.mozilla.vp6player { + import flash.display.MovieClip; + import flash.display.LoaderInfo; + import flash.external.ExternalInterface; + import flash.media.Video; + import flash.net.NetConnection; + import flash.net.NetStream; + import flash.net.NetStreamAppendBytesAction; + import flash.events.NetStatusEvent; + import flash.events.SecurityErrorEvent; + import flash.events.AsyncErrorEvent; + import flash.system.Security; + import flash.utils.ByteArray; + + public class VP6Player extends MovieClip { + private var _video:Video; + + private var _connection:NetConnection; + private var _stream:NetStream; + private var _mediaUrl:String; + private var _duration:Number; + private var _volume:Number; + private var _id:String; + private var _buffer:ByteArray; + + public function VP6Player() { + _video = new Video(); + addChild(_video); + + var params:Object = loaderInfo.parameters; + _id = params['id']; + initExternalMethods(); + + ExternalInterface.call('Shumway.GFX.VP6Player.__init', _id); + _volume = 1.0; + syncProperties({paused: true, volume: _volume}); + } + + private function initExternalMethods():void { + Security.allowDomain('*'); // Flash has file:/// origin -- allowing resource:// access us. + ExternalInterface.addCallback('setProperty', this._setProperty); + ExternalInterface.addCallback('play', this._play); + ExternalInterface.addCallback('pause', this._pause); + ExternalInterface.addCallback('getTime', this._getTime); + ExternalInterface.addCallback('setTime', this._setTime); + ExternalInterface.addCallback('getBufferedTime', this._getBufferedTime); + ExternalInterface.addCallback('getBufferedTime', this._getBufferedTime); + ExternalInterface.addCallback('addVP6Data', this._addVP6Data); + } + + private function _setProperty(propertyName:String, value:*):void { + switch (propertyName) { + case 'src': + if (this._mediaUrl != value) { + this._mediaUrl = value; + this._connection = null; + } + break; + case 'volume': + _volume = value; + if (!_stream) { + break; + } + var soundTransform = _stream.soundTransform; + soundTransform.volume = value; + _stream.soundTransform = soundTransform; + break; + } + } + + private function _getTime():Number { + return _stream.time; + } + + private function _setTime(offset:Number):void { + _stream.seek(offset); + } + + private function _getBufferedTime():Number { + return _stream.bytesLoaded / _stream.bytesTotal * _duration; + } + + private static const base64DecodeMap:Array = [ // starts at 0x2B + 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 0, 0, 0, 0, 0, 0, 0, // 0x3A-0x40 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, // 0x5B-0x0x60 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51]; + private static const base64DecodeMapOffset:int = 0x2B; + private static const base64EOF:int = 0x3D; + + private static function _decodeBase64(encoded:String, decoded:ByteArray):void { + var ch:int, code:int, code2:int; + var i:int = 0; + while (i < encoded.length) { + ch = encoded.charCodeAt(i++); + code = base64DecodeMap[ch - base64DecodeMapOffset]; + ch = encoded.charCodeAt(i++); + code2 = base64DecodeMap[ch - base64DecodeMapOffset]; + decoded.writeByte((code << 2) | ((code2 & 0x30) >> 4)); + + ch = encoded.charCodeAt(i++); + if (ch == base64EOF) { + break; + } + code = base64DecodeMap[ch - base64DecodeMapOffset]; + decoded.writeByte(((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2)); + + ch = encoded.charCodeAt(i++); + if (ch == base64EOF) { + break; + } + code2 = base64DecodeMap[ch - base64DecodeMapOffset]; + decoded.writeByte(((code & 0x03) << 6) | code2); + } + } + + private function _addVP6Data(data:String):void { + if (data === null) { + if (_stream) { + _stream.appendBytesAction(NetStreamAppendBytesAction.END_SEQUENCE); + } + // TODO cache for !_stream + return; + } + var decoded:ByteArray = _buffer || new ByteArray(); + _decodeBase64(data, decoded); + if (_stream) { + decoded.position = 0; + _stream.appendBytes(decoded); + _buffer = null; + } else { + _buffer = decoded; + } + } + + private function netStatusHandler(event:NetStatusEvent):void { + switch (event.info.code) { + case "NetConnection.Connect.Success": + connectStream(); + break; + case "NetStream.Play.Start": + syncProperties({paused: false}); + dispatchPlayerEvent('loadeddata'); + break; + case "NetStream.Play.Stop": + dispatchPlayerEvent('ended'); + break; + case "NetStream.Buffer.Full": + dispatchPlayerEvent('canplay'); + break; + case "NetStream.Buffer.Empty": + //TODO + break; + case "NetStream.Pause.Notify": + syncProperties({paused: true}); + dispatchPlayerEvent('pause'); + break; + case "NetStream.Unpause.Notify": + syncProperties({paused: false}); + dispatchPlayerEvent('play'); + break; + case "NetStream.Seek.Notify": + dispatchPlayerEvent('seeking'); + break; + case "NetStream.Seek.Complete": + dispatchPlayerEvent('seeked'); + break; + case "NetStream.Play.NoSupportedTrackFound": + syncProperties({errorCode: 4}); + dispatchPlayerEvent('error'); + break; + case "NetStream.Play.FileStructureInvalid": + syncProperties({errorCode: 3}); + dispatchPlayerEvent('error'); + break; + case "NetStream.Play.StreamNotFound": + syncProperties({errorCode: 2}); + dispatchPlayerEvent('error'); + break; + } + } + + private function securityErrorHandler(event:SecurityErrorEvent):void { + trace("securityErrorHandler: " + event); + } + + private function asyncErrorHandler(event:AsyncErrorEvent):void { + // ignore AsyncErrorEvent events. + } + + private function connectStream() { + _stream = new NetStream(_connection); + _stream.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); + _stream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler); + _stream.client = { + onMetaData: this.onMetaData + }; + _duration = NaN; + var soundTransform = _stream.soundTransform; + soundTransform.volume = _volume; + _stream.soundTransform = soundTransform; + + _video.attachNetStream(_stream); + + var isStream = _mediaUrl.indexOf('flvstream:') == 0; + if (isStream) { + _stream.play(null); + _stream.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN); + if (_buffer) { + _buffer.position = 0; + _stream.appendBytes(_buffer); + _buffer = null; + } + } else { + _stream.play(_mediaUrl); + + } + } + + private function onMetaData(metadata:*) { + syncProperties({ + videoWidth: metadata.width, + videoHeight: metadata.height, + duration: metadata.duration + }); + _duration = metadata.duration; + ExternalInterface.call('Shumway.GFX.VP6Player.__setSize', _id, metadata.width, metadata.height); + dispatchPlayerEvent('loadedmetadata'); + } + + private function _play():void { + if (!_connection) { + _connection = new NetConnection(); + _connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); + _connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); + _connection.connect(null); + return; + } + + _stream.resume(); + } + + private function _pause():void { + _stream.pause(); + } + + private function syncProperties(props:*):void { + ExternalInterface.call('Shumway.GFX.VP6Player.__syncProperties', _id, props); + } + + private function dispatchPlayerEvent(eventName:String):void { + ExternalInterface.call('Shumway.GFX.VP6Player.__dispatchEvent', _id, eventName); + } + } +} diff --git a/utils/vp6player/vp6player.fla b/utils/vp6player/vp6player.fla new file mode 100644 index 0000000000..a0a92ff4ec Binary files /dev/null and b/utils/vp6player/vp6player.fla differ diff --git a/utils/vp6player/vp6player.swf b/utils/vp6player/vp6player.swf new file mode 100755 index 0000000000..83457e8c63 Binary files /dev/null and b/utils/vp6player/vp6player.swf differ