From 46e54c44df1c2fae605da8694b3cf2eb53b00f31 Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Wed, 11 Apr 2012 08:53:30 -0400 Subject: [PATCH] Huge overhaul. - fix flaws with videotube's QTE-finding algorithm which caused breakage when seeking around in the video, qtes were added, or anyone sneezed - switch from arbitrary polygons to simple circles, simplifying the editing interface considerably - support for deletes and overwrites of QTEs - new server protocol - pause/resume when focus lost - some degree of error handling - restart key - videotube API no longer autostarts; 'play' becomes 'enqueue' and is called once --- LaserTube.as3proj | 11 +++- src/ClickArea.as | 38 +++++-------- src/Game.as | 115 +++++++++++++++++++++++++++++++++++++++ src/GameEditor.as | 126 ++++++++++++++++++++++++++++-------------- src/GamePlayer.as | 67 ++++++++++------------- src/Gamedisc.as | 130 ++++++++++++++++++++++++++++++++++++++------ src/Main.as | 42 ++++++++++++-- src/Qte.as | 55 +++++++++++++------ src/Util.as | 35 +++++++++++- src/Videotube.as | 91 ++++++++++++++++++++++++------- src/VideotubeFlv.as | 10 +++- src/VideotubeYt.as | 20 +++++-- 12 files changed, 564 insertions(+), 176 deletions(-) create mode 100644 src/Game.as diff --git a/LaserTube.as3proj b/LaserTube.as3proj index 04258dd..380e420 100644 --- a/LaserTube.as3proj +++ b/LaserTube.as3proj @@ -1,14 +1,16 @@  - + - + + + @@ -24,6 +26,7 @@ \ No newline at end of file diff --git a/src/ClickArea.as b/src/ClickArea.as index 807c5c7..b5d1309 100644 --- a/src/ClickArea.as +++ b/src/ClickArea.as @@ -13,38 +13,26 @@ package private var shape:Shape; private var shapeHidden:Shape; private var fHidden:Boolean; - public function ClickArea(rgpoint: Array, color:uint, alpha:Number = 1) + public function ClickArea(center: Point, radius:Number, color:uint, alpha:Number = 1) { super(); - shape = drawShape(new Shape(), rgpoint, color, null, alpha); - shapeHidden = drawShape(new Shape(), rgpoint, color, null, 0); + shape = drawShape(new Shape(), new Point(0,0), radius, color, null, alpha); + shapeHidden = drawShape(new Shape(), new Point(0,0), radius, color, null, 0); fHidden = false; + moveTo(center); addChild(shape); } - public static function drawShape(shape:Shape, rgpoint:Array, color:uint, colorLine:* = null, alpha:Number = 1):Shape + public function moveTo(centerNew: Point):void + { + x = centerNew.x; + y = centerNew.y; + } + public static function drawShape(shape:Shape, center:Point, radius:Number, color:uint, colorLine:* = null, alpha:Number = 1):Shape { shape.graphics.clear(); - if (rgpoint.length > 0) - { - if (colorLine != null) - shape.graphics.lineStyle(1, colorLine); - shape.graphics.beginFill(color, alpha); - var fFirstPoint:Boolean = true; - for each (var point:Point in rgpoint) - { - if (fFirstPoint) - { - shape.graphics.moveTo(point.x, point.y); - fFirstPoint = false; - } - else - { - shape.graphics.lineTo(point.x, point.y); - } - } - shape.graphics.lineTo(rgpoint[0].x, rgpoint[0].y); - shape.graphics.endFill(); - } + shape.graphics.beginFill(color, alpha); + shape.graphics.drawCircle(center.x, center.y, radius); + shape.graphics.endFill(); return shape; } public function Show():void diff --git a/src/Game.as b/src/Game.as new file mode 100644 index 0000000..ec86c53 --- /dev/null +++ b/src/Game.as @@ -0,0 +1,115 @@ +package +{ + import flash.display.Sprite; + import flash.events.Event; + import flash.text.TextField; + import flash.text.TextFormat; + + /** + * ... + * @author Jeremy Penner + */ + public class Game extends Sprite + { + public static var hasFocus:Boolean = false; + + protected var videotube:Videotube; + protected var gamedisc:Gamedisc; + protected var clickarea:ClickArea; + protected var text:TextField; + protected var textValues: Array; + public function Game(videotube:Videotube, gamedisc:Gamedisc) + { + this.videotube = videotube; + this.gamedisc = gamedisc; + addEventListener(Event.ADDED_TO_STAGE, init); + } + protected function init(e:Event):void { + trace("init"); + removeEventListener(Event.ADDED_TO_STAGE, init); + addEventListener(Event.REMOVED_FROM_STAGE, cleanup); + + stage.addEventListener(Event.ACTIVATE, onFocus); + stage.addEventListener(Event.DEACTIVATE, onLoseFocus); + + clickarea = null; + if (text != null) + removeChild(text); + text = null; + textValues = []; + + if (hasFocus) { + onFocus(null); + } else { + onLoseFocus(null); + } + } + protected function cleanup(e:Event):void { + trace("cleanup"); + removeEventListener(Event.REMOVED_FROM_STAGE, cleanup); + + stage.removeEventListener(Event.ACTIVATE, onFocus); + stage.removeEventListener(Event.DEACTIVATE, onLoseFocus); + + if (hasFocus) + onPause(); + clearClickarea(); + } + protected function onFocus(e:Event):void { + trace("received focus"); + popText(); + hasFocus = true; + stage.addEventListener(Event.EXIT_FRAME, onFocusEnded); + } + protected function onFocusEnded(e:Event): void { + stage.removeEventListener(Event.EXIT_FRAME, onFocusEnded); + onResume(); + } + protected function onLoseFocus(e:Event): void { + trace("lost focus"); + pushText("Paused\nCLICK TO PLAY", 0x000000, 0xFFFFFF); + hasFocus = false; + onPause(); + } + + protected function onPause(): void { + trace("onPause"); + videotube.pause(); + } + protected function onResume(): void { + trace("onResume"); + if (fPlaying()) + videotube.resume(); + } + protected function fPlaying(): Boolean { + return false; + } + public function clearClickarea(): void + { + if (clickarea != null) { + removeChild(clickarea); + clickarea = null; + } + } + protected function pushText(html:String, bgcolor:int, fgcolor:int): void { + if (text == null) + text = Util.addTextFieldFullScreen(this); + textValues.push( { 'html': html, 'bgcolor':bgcolor, 'fgcolor':fgcolor } ); + trace("saying: " + html); + Util.setText(text, html, 164, bgcolor, fgcolor); + } + protected function popText(): void { + if (textValues.length > 0) { + textValues.pop(); + if (textValues.length > 0) { + var val:Object = textValues.pop(); + pushText(val['html'], val['bgcolor'], val['fgcolor']); + } else { + removeChild(text); + text = null; + } + } + } + } + +} \ No newline at end of file diff --git a/src/GameEditor.as b/src/GameEditor.as index 908ae90..e28d7f7 100644 --- a/src/GameEditor.as +++ b/src/GameEditor.as @@ -4,69 +4,111 @@ package import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; + import flash.geom.Point; /** * ... * @author jjp */ - public class GameEditor extends Sprite + public class GameEditor extends Game { - private var videotube:Videotube; - private var gamedisc:Gamedisc; - private var sketchShape:SketchShape; - private var clickarea:ClickArea; - - public function GameEditor(videotube:Videotube, gamedisc:Gamedisc) - { - this.videotube = videotube; - this.gamedisc = gamedisc; - addEventListener(Event.ADDED_TO_STAGE, init); + protected var qte:Qte; + public override function GameEditor(videotube:Videotube, gamedisc:Gamedisc) { + super(videotube, gamedisc); + qte = null; } - public function init(e: Event):void + protected override function onResume():void { - removeEventListener(Event.ADDED_TO_STAGE, init); - addEventListener(Event.REMOVED_FROM_STAGE, cleanup); + super.onResume(); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); - - sketchShape = new SketchShape(); - addChild(sketchShape); - sketchShape.addEventListener(SketchShape.DRAW_BEGIN, onDrawBegin); - sketchShape.addEventListener(SketchShape.DRAW_END, onDrawEnd); - - clickarea = null; + stage.addEventListener(MouseEvent.MOUSE_DOWN, onClick); + stage.addEventListener(MouseEvent.MOUSE_MOVE, onDrag); + videotube.addEventListener(EventQte.QTE, onQteBegin); videotube.addEventListener(EventQte.QTE_TIMEOUT, onQteEnd); + videotube.addEventListener(EventQte.QTE_CANCEL, onQteEnd); } - public function cleanup(e: Event):void + protected override function onPause():void { - removeEventListener(Event.REMOVED_FROM_STAGE, cleanup); + super.onPause(); stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); - sketchShape.removeEventListener(SketchShape.DRAW_BEGIN, onDrawBegin); - sketchShape.removeEventListener(SketchShape.DRAW_END, onDrawEnd); + stage.removeEventListener(MouseEvent.MOUSE_DOWN, onClick); + stage.removeEventListener(MouseEvent.MOUSE_MOVE, onDrag); + + videotube.removeEventListener(EventQte.QTE, onQteBegin); + videotube.removeEventListener(EventQte.QTE_TIMEOUT, onQteEnd); + videotube.removeEventListener(EventQte.QTE_CANCEL, onQteEnd); + } + protected override function fPlaying():Boolean { + return true; } public function onQteBegin(e: EventQte):void { - clickarea = new ClickArea(e.qte.rgpoint, 0x4444ee, 0.4); - addChild(clickarea); + trace("editor qte begin"); + if (e.qte != qte) { + Util.assert(qte == null); + if (qte != null) { + gamedisc.repostQte(qte); + clearClickarea(); + qte = null; + } + addClickArea(e.qte); + } } public function onQteEnd(e: EventQte):void { - removeChild(clickarea); - clickarea = null; - } - public function onKeyUp(key: KeyboardEvent):void - { - - } - private function onDrawBegin(e: Event): void - { - videotube.pause(); - } - private function onDrawEnd(e: Event): void - { - gamedisc.AddQte(new Qte(sketchShape.rgpoint, videotube.time())); - videotube.resume(); + trace("editor qte end"); + Util.assert(qte == e.qte); + gamedisc.repostQte(qte); + clearClickarea(); + qte = null; } + public function onKeyUp(key: KeyboardEvent):void + { + if (key.keyCode == 46 && qte != null) {// delete + gamedisc.DeleteQte(qte, videotube); + clearClickarea(); + qte = null; + } else if (key.keyCode == 37) { // leftArrow + videotube.seek(videotube.time() - 3); + clearClickarea(); + qte = null; + } else if (key.keyCode == 39) { // rightArrow + videotube.seek(videotube.time() + 3); + clearClickarea(); + qte = null; + } + } + private function addClickArea(qteNew: Qte):void + { + qte = qteNew; + clickarea = new ClickArea(qte.center, qte.radius, 0x4444ee, 0.4); + addChild(clickarea); + } + private function moveClickArea(point:Point): void + { + if (qte) { + qte.moveTo(point); + clickarea.moveTo(point); + } + } + public function onClick(e: MouseEvent):void + { + trace("clicked"); + var point:Point = new Point(e.stageX, e.stageY); + if (qte) + moveClickArea(point); + else { + var qte:Qte = new Qte(point, 75, videotube.time(), -1, true /*fDirty*/); + addClickArea(qte); + gamedisc.AddQte(qte, videotube); + } + } + public function onDrag(e: MouseEvent):void + { + if (e.buttonDown) + moveClickArea(new Point(e.stageX, e.stageY)); + } } } \ No newline at end of file diff --git a/src/GamePlayer.as b/src/GamePlayer.as index 3dc0225..6d46d55 100644 --- a/src/GamePlayer.as +++ b/src/GamePlayer.as @@ -3,6 +3,7 @@ package import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; + import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.geom.Point; import flash.text.StyleSheet; @@ -13,56 +14,45 @@ package * ... * @author jjp */ - public class GamePlayer extends Sprite + public class GamePlayer extends Game { - private var videotube:Videotube; - private var gamedisc:Gamedisc; - private var clickarea:ClickArea; - private var textDeath:TextField; - - public function GamePlayer(videotube:Videotube, gamedisc:Gamedisc) - { - this.videotube = videotube; - this.gamedisc = gamedisc; - clickarea = null; - textDeath = null; - addEventListener(Event.ADDED_TO_STAGE, init); + private var fAlive: Boolean; + public override function GamePlayer(videotube:Videotube, gamedisc:Gamedisc) { + super(videotube, gamedisc); + fAlive = true; } - private function init(e:Event):void + protected override function onResume():void { - removeEventListener(Event.ADDED_TO_STAGE, init); - addEventListener(Event.REMOVED_FROM_STAGE, cleanup); - + super.onResume(); addEventListener(MouseEvent.CLICK, onClick); videotube.addEventListener(EventQte.QTE, onQte); videotube.addEventListener(EventQte.QTE_TIMEOUT, onTimeout); + stage.addEventListener(KeyboardEvent.KEY_UP, onKey); } - private function cleanup(e:Event):void + protected override function onPause():void { - removeEventListener(Event.REMOVED_FROM_STAGE, cleanup); + super.onPause(); removeEventListener(MouseEvent.CLICK, onClick); videotube.removeEventListener(EventQte.QTE, onQte); + videotube.removeEventListener(EventQte.QTE_TIMEOUT, onTimeout); + stage.removeEventListener(KeyboardEvent.KEY_UP, onKey); + } + protected override function fPlaying():Boolean { + return fAlive; } private function onQte(e:EventQte):void { + trace(e.qte.secTrigger() + "gameplayer start" + e.qte.secTimeout()); clearClickarea(); - clickarea = new ClickArea(e.qte.rgpoint, 0xffff00, 0.7); + clickarea = new ClickArea(e.qte.center, e.qte.radius, 0x4444ee, 0.4); addChild(clickarea); } private function onTimeout(e:EventQte):void { - if (clickarea != null) - { - videotube.pause(); - textDeath = new TextField(); - textDeath.htmlText = "

YOU ARE DEAD

"; - textDeath.wordWrap = true; - textDeath.background = true; - textDeath.backgroundColor = 0x0000FF; - textDeath.width = stage.stageWidth; - textDeath.height = stage.stageHeight; - textDeath.setTextFormat(new TextFormat(null, 164, 0xFF0000)); - addChild(textDeath); + trace("gameplayer timeout"); + if (clickarea != null) { + pushText("OH SHIT\n\nhit R to restart", 0x0000FF, 0xFF0000); + fAlive = false; } clearClickarea(); } @@ -71,13 +61,14 @@ package if (clickarea != null && clickarea.FHit(new Point(mouse.stageX, mouse.stageY))) clearClickarea(); } - private function clearClickarea():void - { - if (clickarea != null) - { - removeChild(clickarea); - clickarea = null; + private function onKey(event:KeyboardEvent):void { + if (!fAlive && event.keyCode == 82) { + fAlive = true; + popText(); + videotube.seek(0); + videotube.resume(); } + trace("key:", event.keyCode); } } diff --git a/src/Gamedisc.as b/src/Gamedisc.as index 6ffe654..b40c270 100644 --- a/src/Gamedisc.as +++ b/src/Gamedisc.as @@ -1,8 +1,12 @@ package { import com.adobe.serialization.json.JSON; + import flash.display.IBitmapDrawable; + import flash.events.Event; import flash.events.EventDispatcher; + import flash.events.IOErrorEvent; import flash.net.sendToURL; + import flash.net.URLLoader; import flash.net.URLRequest; import flash.net.URLRequestHeader; import flash.net.URLRequestMethod; @@ -17,29 +21,121 @@ package public static const VIDEOTUBE_YOUTUBE:String = "yt"; public var urlVideo:String; - public var urlPostQte:String; - public var headerPostQte:Object; public var typeVideotube:String; public var rgqte:Array; + + protected var urlPostQte:String; + protected var csrf:String; + protected var queuePost:Array; + public function Gamedisc(urlVideo:String = null, typeVideotube:String = null) { this.urlVideo = urlVideo; this.typeVideotube = typeVideotube; this.rgqte = []; + this.urlPostQte = null; + this.csrf = null; + this.queuePost = []; } - public function AddQte(qte:Qte):void + public function setUrlPost(urlPostQte:String, csrf:String):void { + trace("setting url", urlPostQte, csrf); + this.urlPostQte = urlPostQte; + this.csrf = csrf; + } + public function fCanEdit():Boolean { + return urlPostQte != null; + } + public function AddQte(qte:Qte, videotube:Videotube):void { - rgqte.splice(Math.abs(Util.binarySearch(rgqte, qte, Qte.compare)), 0, qte); - if (urlPostQte != null) - { + var iqte:int = Math.abs(Util.binarySearch(rgqte, qte, Qte.compare)); + var cqteDrop:int = 0; + while (true) { + var iqteToDrop:int = iqte + cqteDrop; + if (iqteToDrop >= rgqte.length) + break; + if (rgqte[iqteToDrop].secTrigger() <= qte.secTimeout()) { + cqteDrop ++; + } else { + break; + } + } + if (iqte > 0 && rgqte[iqte - 1].secTimeout() >= qte.secTrigger()) { + iqte --; + cqteDrop ++; + } + rgqte.splice(iqte, cqteDrop, qte); + + Util.assert(qte.secTrigger() == videotube.time()); + videotube.onQtesChanged(iqte + 1, qte); + } + public function DeleteQte(qte:Qte, videotube:Videotube):void + { + var iqte:int = Util.binarySearch(rgqte, qte, Qte.compare); + Util.assert(iqte >= 0); + rgqte.splice(iqte, 1); + + post("delete", { 'ms_trigger': qte.msTrigger } ); + videotube.onQtesChanged(iqte, null); + } + protected function post(action: String, val:Object):void { + if (urlPostQte != null) { + val['action'] = action; + val['csrf'] = csrf; + queuePost.push(JSON.encode(val)); + if (queuePost.length == 1) { + doNextPost(); + } + } + } + protected function doNextPost(): void { + if (queuePost.length > 0) { var req:URLRequest = new URLRequest(urlPostQte); req.method = URLRequestMethod.POST; - for (var key:String in headerPostQte) - req.requestHeaders.push(new URLRequestHeader(key, headerPostQte[key])); - var data:URLVariables = new URLVariables(); - data.qte = JSON.encode(qte.ToJson()); - req.data = data; - sendToURL(req); + req.data = queuePost.shift(); + req.contentType = 'application/json'; + var loader:URLLoader = new URLLoader(); + loader.addEventListener(Event.COMPLETE, onPostComplete); + loader.addEventListener(IOErrorEvent.IO_ERROR, onPostFailed); + loader.load(req); + } + } + protected function onPostComplete(event:Event): void { + event.target.removeEventListener(Event.COMPLETE, onPostComplete); + event.target.removeEventListener(IOErrorEvent.IO_ERROR, onPostFailed); + try { + var result:Object = JSON.decode(event.target.data); + csrf = result.csrf; + + if (result.err != 'ok') { + if (result.err == 'invalid') { + reportError("Sorry, another browser has begun editing the video."); + } else if (result.err == 'expired') { + reportError("Sorry, your editing session has timed out."); + } else { + reportError(); + } + } else { + doNextPost(); + } + } catch (e:Error) { + trace(e); + reportError(); + } + } + protected function onPostFailed(event:Event): void { + event.target.removeEventListener(Event.COMPLETE, onPostComplete); + event.target.removeEventListener(IOErrorEvent.IO_ERROR, onPostFailed); + reportError(); + } + protected function reportError(error:String = "Sorry, something weird happened. It's my fault. Reload the page to try again."): void { + Main.instance.fatalError(error); + } + public function repostQte(qte:Qte): void + { + if (qte.fDirty && urlPostQte != null) + { + post("put", { 'qte': qte.ToJson() } ); + qte.fDirty = false; } } public function CreateVideotube():Videotube @@ -60,19 +156,17 @@ package json.urlPostQte = urlPostQte; return json; } - public function FromJson(json:Object, jsonPostHeaders:Object):void + public function FromJson(json:Object):void { rgqte = []; - for each (var jsonQte:Object in json.rgqte) + for each (var jsonQte:Object in json.qtes) { var qte:Qte = new Qte(); qte.FromJson(jsonQte); rgqte.push(qte); } - urlVideo = json.urlVideo; - typeVideotube = json.typeVideotube; - urlPostQte = json.urlPostQte; - headerPostQte = jsonPostHeaders; + urlVideo = json.url; + typeVideotube = json.ktube; } } diff --git a/src/Main.as b/src/Main.as index 2f4ce45..07c57c0 100644 --- a/src/Main.as +++ b/src/Main.as @@ -7,6 +7,7 @@ package import flash.events.MouseEvent; import flash.external.ExternalInterface; import flash.geom.Point; + import flash.text.TextField; import flash.ui.Keyboard; /** @@ -19,11 +20,25 @@ package private var gamedisc:Gamedisc; private var gameeditor:GameEditor; private var gameplayer:GamePlayer; + private var debug:Boolean; + + public static var instance:Main; public function Main():void { - gamedisc = new Gamedisc(); - gamedisc.FromJson(JSON.decode(loaderInfo.parameters.jsonDisc), JSON.decode(loaderInfo.parameters.jsonPostHeaders)); + instance = this; + if (loaderInfo.parameters.jsonDisc) { + gamedisc = new Gamedisc(); + gamedisc.FromJson(JSON.decode(loaderInfo.parameters.jsonDisc)); + trace("+++LOADING+++", loaderInfo.parameters.urlPostQte, loaderInfo.parameters.csrf); + if (loaderInfo.parameters.urlPostQte && loaderInfo.parameters.csrf) { + gamedisc.setUrlPost(JSON.decode(loaderInfo.parameters.urlPostQte), JSON.decode(loaderInfo.parameters.csrf)); + } + } else { + // debugging + gamedisc = new Gamedisc("The%20Last%20Eichhof%20-%20Longplay.flv", Gamedisc.VIDEOTUBE_FLV); + debug = true; + } videotube = gamedisc.CreateVideotube(); if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); @@ -61,19 +76,34 @@ package gameplayer = new GamePlayer(videotube, gamedisc); addChild(gameplayer); } - videotube.seek(0); + videotube.enqueue(); } private function onVideotubeReady(event:Event = null):void { toggleGame(); - if (gamedisc.urlPostQte == null) + if (!gamedisc.fCanEdit()) toggleGame(); - videotube.play(); } private function onKey(key:KeyboardEvent):void { - if (key.keyCode == Keyboard.SPACE && gamedisc.urlPostQte != null) + if (key.keyCode == Keyboard.SPACE && debug) toggleGame(); } + public function fatalError(message:String):void { + var text:TextField = Util.addTextFieldFullScreen(this); + Util.setText(text, message, 64, 0xffffff, 0x440000); + if (gameplayer) + removeChild(gameplayer); + if (gameeditor) + removeChild(gameeditor); + if (videotube) { + videotube.pause(); + removeChild(videotube); + } + gameplayer = null; + gameeditor = null; + videotube = null; + stage.removeEventListener(KeyboardEvent.KEY_UP, onKey); + } } } \ No newline at end of file diff --git a/src/Qte.as b/src/Qte.as index a08860a..a60207b 100644 --- a/src/Qte.as +++ b/src/Qte.as @@ -7,39 +7,62 @@ package */ public class Qte { - public var rgpoint:Array; - public var secTrigger:Number; + public var center:Point; + public var radius:Number; + public var msTrigger:int; + public var msTimeout:int; + public var fDirty:Boolean; - public function Qte(rgpoint: Array = null, secTrigger:Number = -1) + public function Qte(center: Point = null, radius: Number = -1, secTrigger:Number = -1, secTimeout:Number = -1, fDirty: Boolean = false) { - this.rgpoint = rgpoint; - this.secTrigger = secTrigger; + this.center = center; + this.radius = radius; + if (secTrigger >= 0) + this.msTrigger = int(secTrigger * 1000); + else + this.msTrigger = -1; + + if (secTimeout >= 0) { + this.msTimeout = int(secTimeout * 1000); + Util.assert(msTimeout > msTrigger); + } else if (msTrigger >= 0) { + msTimeout = msTrigger + 1000; // 1 second timeout by default + } else { + msTimeout = -1; + } + this.fDirty = fDirty; + } + public function moveTo(center: Point): void + { + fDirty = true; + this.center = center; } public static function compare(qte1:Qte, qte2:Qte):int { - if (qte1.secTrigger == qte2.secTrigger) + if (qte1.msTrigger == qte2.msTrigger) return 0; - else if (qte1.secTrigger < qte2.secTrigger) + else if (qte1.msTrigger < qte2.msTrigger) return -1; return 1; } + public function secTrigger():Number + { + return msTrigger / 1000.0; + } public function secTimeout():Number { - return secTrigger + 1.0; + return secTrigger() + 1.0; } public function ToJson():Object { - var jsonRgpoint:Array = []; - for each(var point:Point in rgpoint) - jsonRgpoint.push([point.x, point.y]); - return { rgpoint: jsonRgpoint, secTrigger: secTrigger }; + return { shape: {center: [center.x, center.y], radius: radius}, ms_trigger: msTrigger, ms_finish: msTimeout }; } public function FromJson(json:Object):void { - rgpoint = [] - for each(var jsonPoint:Array in json.rgpoint) - rgpoint.push(new Point(jsonPoint[0], jsonPoint[1])); - secTrigger = json.secTrigger; + center = new Point(json.shape.center[0], json.shape.center[1]); + radius = json.shape.radius; + msTrigger = json.ms_trigger; + msTimeout = json.ms_finish; } } } \ No newline at end of file diff --git a/src/Util.as b/src/Util.as index 29bf8a8..158cbc7 100644 --- a/src/Util.as +++ b/src/Util.as @@ -1,7 +1,15 @@ package { import com.adobe.serialization.json.JSON; + import flash.display.Bitmap; + import flash.display.BitmapData; + import flash.display.DisplayObject; + import flash.display.DisplayObjectContainer; + import flash.display.Shape; + import flash.display.Sprite; import flash.external.ExternalInterface; + import flash.text.TextField; + import flash.text.TextFormat; /** * ... * @author jjp @@ -33,6 +41,31 @@ package { ExternalInterface.call("alert", JSON.encode(rgo)); } + public static function assert(cond:Boolean, msg:String = "Assertion failed"): void + { + if (!cond) { + throw new Error(msg); + } + } + public static function BitmapFromSprite(sprite: DisplayObject):Bitmap + { + var bitmapData:BitmapData = new BitmapData(sprite.width, sprite.height); + bitmapData.draw(sprite); + return new Bitmap(bitmapData); + } + public static function addTextFieldFullScreen(parent: DisplayObjectContainer): TextField { + var text:TextField = new TextField(); + text.wordWrap = true; + text.background = true; + text.width = parent.stage.stageWidth; + text.height = parent.stage.stageHeight; + parent.addChild(text); + return text; + } + public static function setText(text:TextField, html:String, size:int, bgcolor:int, fgcolor:int):void { + text.htmlText = "

" + html + "

"; + text.backgroundColor = bgcolor; + text.setTextFormat(new TextFormat(null, size, fgcolor)); + } } - } \ No newline at end of file diff --git a/src/Videotube.as b/src/Videotube.as index e45cd51..6e8283b 100644 --- a/src/Videotube.as +++ b/src/Videotube.as @@ -3,6 +3,8 @@ package import flash.display.Sprite; import flash.events.Event; import flash.events.EventDispatcher; + import flash.text.TextField; + import flash.text.TextFormat; /** * ... @@ -11,9 +13,10 @@ package public class Videotube extends Sprite { public static const READY:String = "videotube-ready"; + private static const DEBUG_VIDEO:Boolean = false; public function fready():Boolean { throw "not implemented"; } - public function play():void { throw "not implemented"; } + public function enqueue():void { throw "not implemented"; } public function pause():void { throw "not implemented"; } public function resume():void { throw "not implemented"; } public function time():Number { throw "not implemented"; } @@ -21,84 +24,132 @@ package private var secPrev:Number; private var iqte:int; - private var iqtePrev:int; + private var qtePrev:Qte; protected var gamedisc:Gamedisc; + private var textDebug:TextField; public function Videotube(gamedisc: Gamedisc) { this.gamedisc = gamedisc; secPrev = 0; iqte = 0; - iqtePrev = 0; + qtePrev = null; + if (DEBUG_VIDEO) { + textDebug = new TextField(); + textDebug.x = 0; + textDebug.y = 0; + textDebug.width = 640; + textDebug.height = 70; + } } public function seek(sec:Number):void { + trace("seeking"); seekI(sec); - if (iqtePrev != -1) - { - dispatchEvent(new EventQte(EventQte.QTE_CANCEL, gamedisc.rgqte[iqtePrev])); - iqtePrev = -1; - } + cancelPrev(); iqte = -1; + trace("seek to " + sec + ":" + iqte); } // returns true if triggered - private function Trigger(iqte:int, ev:String, secNow:Number): Boolean + private function Trigger(qte:Qte, ev:String, secNow:Number): Boolean { - if (iqte >= 0 && iqte < gamedisc.rgqte.length) + if (qte != null) { - var qte:Qte = gamedisc.rgqte[iqte]; var secQte:Number; if (ev == EventQte.QTE) - secQte = qte.secTrigger; + secQte = qte.secTrigger(); else secQte = qte.secTimeout(); + //trace("testing " + iqte + " for " + ev + " " + secQte + " in " + secPrev + ":" + secNow); if (Util.FInTimespan(secQte, secPrev, secNow)) { + trace("triggered " + ev); dispatchEvent(new EventQte(ev, qte)); return true; } } return false; } + protected function cancelPrev():void { + if (qtePrev != null) { + dispatchEvent(new EventQte(EventQte.QTE_CANCEL, qtePrev)); + qtePrev = null; + } + } protected function tick(e: Event):void { var secNow:Number = time(); var fPrevQteProcessed:Boolean = false; var fQteProcessed:Boolean = false; + if (secNow < secPrev) { + iqte = -1; + cancelPrev(); + secPrev = secNow; + } if (iqte < 0) { - iqte = Util.binarySearch(gamedisc.rgqte, secNow, function(qte:Qte, secNow:Number):int { - if (qte.secTrigger < secNow) + trace("finding qte for time " + secNow); + iqte = Math.abs(Util.binarySearch(gamedisc.rgqte, secNow, function(qte:Qte, secNow:Number):int { + if (qte.secTrigger() < secNow) return -1; - if (qte.secTrigger > secNow) + if (qte.secTrigger() > secNow) return 1; return 0; - }); - iqtePrev = iqte; + })); + cancelPrev(); + if (iqte < gamedisc.rgqte.length) + trace("found: " + iqte + " at " + gamedisc.rgqte[iqte].secTrigger()); + else + trace("at end of qtes"); } else { // we loop here so that, in the event of being passed bad data, we still do something vaguely sensible. while (!fPrevQteProcessed && !fQteProcessed) { - if (Trigger(iqtePrev, EventQte.QTE_TIMEOUT, secNow)) + //trace(secNow + ":: prev" + iqtePrev + ", iqte" + iqte); + if (Trigger(qtePrev, EventQte.QTE_TIMEOUT, secNow)) { fPrevQteProcessed = false; - iqtePrev ++; + qtePrev = null; } else fPrevQteProcessed = true; - if (Trigger(iqte, EventQte.QTE, secNow)) + var qte:Qte = null; + if (iqte >= 0 && iqte < gamedisc.rgqte.length) + qte = gamedisc.rgqte[iqte]; + if (Trigger(qte, EventQte.QTE, secNow)) { + Util.assert(qtePrev == null); fQteProcessed = false; iqte ++; + qtePrev = qte; } else fQteProcessed = true; } } + if (DEBUG_VIDEO) { + var txt:String = int(secNow * 1000) + "ms"; + if (iqte < gamedisc.rgqte.length) + txt += " | next qte at " + gamedisc.rgqte[iqte].msTrigger; + if (qtePrev != null) + txt += " | qte over at " + qtePrev.msTimeout; + textDebug.htmlText = txt; + textDebug.backgroundColor = 0x000000; + textDebug.setTextFormat(new TextFormat(null, 16, 0xffffff)); + if (textDebug.parent == this) + removeChild(textDebug); + addChild(textDebug); + } + secPrev = secNow; } + public function onQtesChanged(iqteNext: int, qtePrev:Qte): void { + iqte = iqteNext; + cancelPrev(); + this.qtePrev = qtePrev; + } } } \ No newline at end of file diff --git a/src/VideotubeFlv.as b/src/VideotubeFlv.as index f2245b0..d8f70f9 100644 --- a/src/VideotubeFlv.as +++ b/src/VideotubeFlv.as @@ -38,11 +38,15 @@ package removeEventListener(Event.REMOVED_FROM_STAGE, cleanup); stage.removeEventListener(Event.ENTER_FRAME, tick); } - public override function fready():Boolean { return true; } - public override function play():void { stream.play(gamedisc.urlVideo); } + public override function fready():Boolean { return true; } public override function pause():void { stream.pause(); } public override function resume():void { stream.resume(); } public override function time():Number { return stream.time; } - public override function seek(sec:Number):void { stream.seek(sec); } + protected override function seekI(sec:Number):void { stream.seek(sec); } + public override function enqueue():void { + seek(0); + stream.play(gamedisc.urlVideo); + stream.pause(); + } } } \ No newline at end of file diff --git a/src/VideotubeYt.as b/src/VideotubeYt.as index 20c810f..2d1568e 100644 --- a/src/VideotubeYt.as +++ b/src/VideotubeYt.as @@ -16,6 +16,7 @@ package { super(gamedisc); Security.allowDomain("www.youtube.com"); + Security.allowDomain("*.ytimage.com"); player = null; loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.INIT, onLoaderInit); @@ -23,12 +24,23 @@ package } private function onLoaderInit(event:Event):void { + trace("yt: loader init"); addChild(loader); loader.contentLoaderInfo.removeEventListener(Event.INIT, onLoaderInit); - loader.content.addEventListener("onReady", onPlayerReady); + player = loader.content; + player.addEventListener("onReady", onPlayerReady); + player.addEventListener("onStateChange", onStateChange); + player.addEventListener("onError", onError); + } + private function onStateChange(event:Event):void { + trace("yt: state " + player.getPlayerState()); + } + private function onError(event:Event):void { + trace("yt: error", Object(event).data); } private function onPlayerReady(event:Event):void { + trace("yt: player ready"); player = loader.content; player.setSize(stage.stageWidth, stage.stageHeight); player.cueVideoById(gamedisc.urlVideo); @@ -36,11 +48,11 @@ package stage.addEventListener(Event.ENTER_FRAME, tick); dispatchEvent(new Event(Videotube.READY)); } - public override function fready():Boolean { return player !== null; } - public override function play():void { player.playVideo(); } + public override function fready():Boolean { return player !== null && player.getPlayerState() >= 0; } + public override function enqueue():void { seek(0); } public override function pause():void { player.pauseVideo(); } public override function resume():void { player.playVideo(); } public override function time():Number { return player.getCurrentTime(); } - public override function seek(sec:Number):void { player.seekTo(sec, true); } + protected override function seekI(sec:Number):void { player.seekTo(sec, true); } } } \ No newline at end of file