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
This commit is contained in:
Jeremy Penner 2012-04-11 08:53:30 -04:00
parent bc576d08b5
commit 46e54c44df
12 changed files with 564 additions and 176 deletions

View file

@ -1,14 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<project version="2">
<!-- Output SWF options -->
<output>
<movie disabled="False" />
<movie outputType="Application" />
<movie input="" />
<movie path="bin\LaserTube.swf" />
<movie fps="30" />
<movie width="800" />
<movie height="600" />
<movie version="10" />
<movie minorVersion="0" />
<movie platform="Flash Player" />
<movie background="#FFFFFF" />
</output>
<!-- Other classes to be compiled into your SWF -->
@ -24,6 +26,7 @@
<option locale="" />
<option loadConfig="" />
<option optimize="True" />
<option omitTraces="True" />
<option showActionScriptWarnings="True" />
<option showBindingWarnings="True" />
<option showInvalidCSS="True" />
@ -39,7 +42,7 @@
<option staticLinkRSL="True" />
<option additional="" />
<option compilerConstants="" />
<option customSDK="" />
<option minorVersion="" />
</build>
<!-- SWC Include Libraries -->
<includeLibraries>
@ -82,4 +85,6 @@
<option showHiddenPaths="False" />
<option testMovie="Default" />
</options>
<!-- Plugin storage -->
<storage />
</project>

View file

@ -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

115
src/Game.as Normal file
View file

@ -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;
}
}
}
}
}

View file

@ -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));
}
}
}

View file

@ -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 = "<p align='center'>YOU ARE DEAD</p>";
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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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 = "<p align='center'>" + html + "</p>";
text.backgroundColor = bgcolor;
text.setTextFormat(new TextFormat(null, size, fgcolor));
}
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

View file

@ -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); }
}
}