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

View file

@ -13,38 +13,26 @@ package
private var shape:Shape; private var shape:Shape;
private var shapeHidden:Shape; private var shapeHidden:Shape;
private var fHidden:Boolean; 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(); super();
shape = drawShape(new Shape(), rgpoint, color, null, alpha); shape = drawShape(new Shape(), new Point(0,0), radius, color, null, alpha);
shapeHidden = drawShape(new Shape(), rgpoint, color, null, 0); shapeHidden = drawShape(new Shape(), new Point(0,0), radius, color, null, 0);
fHidden = false; fHidden = false;
moveTo(center);
addChild(shape); 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(); shape.graphics.clear();
if (rgpoint.length > 0) shape.graphics.beginFill(color, alpha);
{ shape.graphics.drawCircle(center.x, center.y, radius);
if (colorLine != null) shape.graphics.endFill();
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();
}
return shape; return shape;
} }
public function Show():void 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.Event;
import flash.events.KeyboardEvent; import flash.events.KeyboardEvent;
import flash.events.MouseEvent; import flash.events.MouseEvent;
import flash.geom.Point;
/** /**
* ... * ...
* @author jjp * @author jjp
*/ */
public class GameEditor extends Sprite public class GameEditor extends Game
{ {
private var videotube:Videotube; protected var qte:Qte;
private var gamedisc:Gamedisc; public override function GameEditor(videotube:Videotube, gamedisc:Gamedisc) {
private var sketchShape:SketchShape; super(videotube, gamedisc);
private var clickarea:ClickArea; qte = null;
public function GameEditor(videotube:Videotube, gamedisc:Gamedisc)
{
this.videotube = videotube;
this.gamedisc = gamedisc;
addEventListener(Event.ADDED_TO_STAGE, init);
} }
public function init(e: Event):void protected override function onResume():void
{ {
removeEventListener(Event.ADDED_TO_STAGE, init); super.onResume();
addEventListener(Event.REMOVED_FROM_STAGE, cleanup);
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
stage.addEventListener(MouseEvent.MOUSE_DOWN, onClick);
sketchShape = new SketchShape(); stage.addEventListener(MouseEvent.MOUSE_MOVE, onDrag);
addChild(sketchShape);
sketchShape.addEventListener(SketchShape.DRAW_BEGIN, onDrawBegin);
sketchShape.addEventListener(SketchShape.DRAW_END, onDrawEnd);
clickarea = null;
videotube.addEventListener(EventQte.QTE, onQteBegin); videotube.addEventListener(EventQte.QTE, onQteBegin);
videotube.addEventListener(EventQte.QTE_TIMEOUT, onQteEnd); 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); stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
sketchShape.removeEventListener(SketchShape.DRAW_BEGIN, onDrawBegin); stage.removeEventListener(MouseEvent.MOUSE_DOWN, onClick);
sketchShape.removeEventListener(SketchShape.DRAW_END, onDrawEnd); 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 public function onQteBegin(e: EventQte):void
{ {
clickarea = new ClickArea(e.qte.rgpoint, 0x4444ee, 0.4); trace("editor qte begin");
addChild(clickarea); 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 public function onQteEnd(e: EventQte):void
{ {
removeChild(clickarea); trace("editor qte end");
clickarea = null; Util.assert(qte == e.qte);
} gamedisc.repostQte(qte);
public function onKeyUp(key: KeyboardEvent):void clearClickarea();
{ qte = null;
}
private function onDrawBegin(e: Event): void
{
videotube.pause();
}
private function onDrawEnd(e: Event): void
{
gamedisc.AddQte(new Qte(sketchShape.rgpoint, videotube.time()));
videotube.resume();
} }
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.Shape;
import flash.display.Sprite; import flash.display.Sprite;
import flash.events.Event; import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent; import flash.events.MouseEvent;
import flash.geom.Point; import flash.geom.Point;
import flash.text.StyleSheet; import flash.text.StyleSheet;
@ -13,56 +14,45 @@ package
* ... * ...
* @author jjp * @author jjp
*/ */
public class GamePlayer extends Sprite public class GamePlayer extends Game
{ {
private var videotube:Videotube; private var fAlive: Boolean;
private var gamedisc:Gamedisc; public override function GamePlayer(videotube:Videotube, gamedisc:Gamedisc) {
private var clickarea:ClickArea; super(videotube, gamedisc);
private var textDeath:TextField; fAlive = true;
public function GamePlayer(videotube:Videotube, gamedisc:Gamedisc)
{
this.videotube = videotube;
this.gamedisc = gamedisc;
clickarea = null;
textDeath = null;
addEventListener(Event.ADDED_TO_STAGE, init);
} }
private function init(e:Event):void protected override function onResume():void
{ {
removeEventListener(Event.ADDED_TO_STAGE, init); super.onResume();
addEventListener(Event.REMOVED_FROM_STAGE, cleanup);
addEventListener(MouseEvent.CLICK, onClick); addEventListener(MouseEvent.CLICK, onClick);
videotube.addEventListener(EventQte.QTE, onQte); videotube.addEventListener(EventQte.QTE, onQte);
videotube.addEventListener(EventQte.QTE_TIMEOUT, onTimeout); 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); removeEventListener(MouseEvent.CLICK, onClick);
videotube.removeEventListener(EventQte.QTE, onQte); 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 private function onQte(e:EventQte):void
{ {
trace(e.qte.secTrigger() + "gameplayer start" + e.qte.secTimeout());
clearClickarea(); clearClickarea();
clickarea = new ClickArea(e.qte.rgpoint, 0xffff00, 0.7); clickarea = new ClickArea(e.qte.center, e.qte.radius, 0x4444ee, 0.4);
addChild(clickarea); addChild(clickarea);
} }
private function onTimeout(e:EventQte):void private function onTimeout(e:EventQte):void
{ {
if (clickarea != null) trace("gameplayer timeout");
{ if (clickarea != null) {
videotube.pause(); pushText("OH SHIT\n\nhit R to restart", 0x0000FF, 0xFF0000);
textDeath = new TextField(); fAlive = false;
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);
} }
clearClickarea(); clearClickarea();
} }
@ -71,13 +61,14 @@ package
if (clickarea != null && clickarea.FHit(new Point(mouse.stageX, mouse.stageY))) if (clickarea != null && clickarea.FHit(new Point(mouse.stageX, mouse.stageY)))
clearClickarea(); clearClickarea();
} }
private function clearClickarea():void private function onKey(event:KeyboardEvent):void {
{ if (!fAlive && event.keyCode == 82) {
if (clickarea != null) fAlive = true;
{ popText();
removeChild(clickarea); videotube.seek(0);
clickarea = null; videotube.resume();
} }
trace("key:", event.keyCode);
} }
} }

View file

@ -1,8 +1,12 @@
package package
{ {
import com.adobe.serialization.json.JSON; import com.adobe.serialization.json.JSON;
import flash.display.IBitmapDrawable;
import flash.events.Event;
import flash.events.EventDispatcher; import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.net.sendToURL; import flash.net.sendToURL;
import flash.net.URLLoader;
import flash.net.URLRequest; import flash.net.URLRequest;
import flash.net.URLRequestHeader; import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod; import flash.net.URLRequestMethod;
@ -17,29 +21,121 @@ package
public static const VIDEOTUBE_YOUTUBE:String = "yt"; public static const VIDEOTUBE_YOUTUBE:String = "yt";
public var urlVideo:String; public var urlVideo:String;
public var urlPostQte:String;
public var headerPostQte:Object;
public var typeVideotube:String; public var typeVideotube:String;
public var rgqte:Array; 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) public function Gamedisc(urlVideo:String = null, typeVideotube:String = null)
{ {
this.urlVideo = urlVideo; this.urlVideo = urlVideo;
this.typeVideotube = typeVideotube; this.typeVideotube = typeVideotube;
this.rgqte = []; 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); var iqte:int = Math.abs(Util.binarySearch(rgqte, qte, Qte.compare));
if (urlPostQte != null) 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); var req:URLRequest = new URLRequest(urlPostQte);
req.method = URLRequestMethod.POST; req.method = URLRequestMethod.POST;
for (var key:String in headerPostQte) req.data = queuePost.shift();
req.requestHeaders.push(new URLRequestHeader(key, headerPostQte[key])); req.contentType = 'application/json';
var data:URLVariables = new URLVariables(); var loader:URLLoader = new URLLoader();
data.qte = JSON.encode(qte.ToJson()); loader.addEventListener(Event.COMPLETE, onPostComplete);
req.data = data; loader.addEventListener(IOErrorEvent.IO_ERROR, onPostFailed);
sendToURL(req); 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 public function CreateVideotube():Videotube
@ -60,19 +156,17 @@ package
json.urlPostQte = urlPostQte; json.urlPostQte = urlPostQte;
return json; return json;
} }
public function FromJson(json:Object, jsonPostHeaders:Object):void public function FromJson(json:Object):void
{ {
rgqte = []; rgqte = [];
for each (var jsonQte:Object in json.rgqte) for each (var jsonQte:Object in json.qtes)
{ {
var qte:Qte = new Qte(); var qte:Qte = new Qte();
qte.FromJson(jsonQte); qte.FromJson(jsonQte);
rgqte.push(qte); rgqte.push(qte);
} }
urlVideo = json.urlVideo; urlVideo = json.url;
typeVideotube = json.typeVideotube; typeVideotube = json.ktube;
urlPostQte = json.urlPostQte;
headerPostQte = jsonPostHeaders;
} }
} }

View file

@ -7,6 +7,7 @@ package
import flash.events.MouseEvent; import flash.events.MouseEvent;
import flash.external.ExternalInterface; import flash.external.ExternalInterface;
import flash.geom.Point; import flash.geom.Point;
import flash.text.TextField;
import flash.ui.Keyboard; import flash.ui.Keyboard;
/** /**
@ -19,11 +20,25 @@ package
private var gamedisc:Gamedisc; private var gamedisc:Gamedisc;
private var gameeditor:GameEditor; private var gameeditor:GameEditor;
private var gameplayer:GamePlayer; private var gameplayer:GamePlayer;
private var debug:Boolean;
public static var instance:Main;
public function Main():void public function Main():void
{ {
gamedisc = new Gamedisc(); instance = this;
gamedisc.FromJson(JSON.decode(loaderInfo.parameters.jsonDisc), JSON.decode(loaderInfo.parameters.jsonPostHeaders)); 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(); videotube = gamedisc.CreateVideotube();
if (stage) init(); if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init); else addEventListener(Event.ADDED_TO_STAGE, init);
@ -61,19 +76,34 @@ package
gameplayer = new GamePlayer(videotube, gamedisc); gameplayer = new GamePlayer(videotube, gamedisc);
addChild(gameplayer); addChild(gameplayer);
} }
videotube.seek(0); videotube.enqueue();
} }
private function onVideotubeReady(event:Event = null):void private function onVideotubeReady(event:Event = null):void
{ {
toggleGame(); toggleGame();
if (gamedisc.urlPostQte == null) if (!gamedisc.fCanEdit())
toggleGame(); toggleGame();
videotube.play();
} }
private function onKey(key:KeyboardEvent):void private function onKey(key:KeyboardEvent):void
{ {
if (key.keyCode == Keyboard.SPACE && gamedisc.urlPostQte != null) if (key.keyCode == Keyboard.SPACE && debug)
toggleGame(); 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 class Qte
{ {
public var rgpoint:Array; public var center:Point;
public var secTrigger:Number; 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.center = center;
this.secTrigger = secTrigger; 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 public static function compare(qte1:Qte, qte2:Qte):int
{ {
if (qte1.secTrigger == qte2.secTrigger) if (qte1.msTrigger == qte2.msTrigger)
return 0; return 0;
else if (qte1.secTrigger < qte2.secTrigger) else if (qte1.msTrigger < qte2.msTrigger)
return -1; return -1;
return 1; return 1;
} }
public function secTrigger():Number
{
return msTrigger / 1000.0;
}
public function secTimeout():Number public function secTimeout():Number
{ {
return secTrigger + 1.0; return secTrigger() + 1.0;
} }
public function ToJson():Object public function ToJson():Object
{ {
var jsonRgpoint:Array = []; return { shape: {center: [center.x, center.y], radius: radius}, ms_trigger: msTrigger, ms_finish: msTimeout };
for each(var point:Point in rgpoint)
jsonRgpoint.push([point.x, point.y]);
return { rgpoint: jsonRgpoint, secTrigger: secTrigger };
} }
public function FromJson(json:Object):void public function FromJson(json:Object):void
{ {
rgpoint = [] center = new Point(json.shape.center[0], json.shape.center[1]);
for each(var jsonPoint:Array in json.rgpoint) radius = json.shape.radius;
rgpoint.push(new Point(jsonPoint[0], jsonPoint[1])); msTrigger = json.ms_trigger;
secTrigger = json.secTrigger; msTimeout = json.ms_finish;
} }
} }
} }

View file

@ -1,7 +1,15 @@
package package
{ {
import com.adobe.serialization.json.JSON; 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.external.ExternalInterface;
import flash.text.TextField;
import flash.text.TextFormat;
/** /**
* ... * ...
* @author jjp * @author jjp
@ -33,6 +41,31 @@ package
{ {
ExternalInterface.call("alert", JSON.encode(rgo)); 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.display.Sprite;
import flash.events.Event; import flash.events.Event;
import flash.events.EventDispatcher; import flash.events.EventDispatcher;
import flash.text.TextField;
import flash.text.TextFormat;
/** /**
* ... * ...
@ -11,9 +13,10 @@ package
public class Videotube extends Sprite public class Videotube extends Sprite
{ {
public static const READY:String = "videotube-ready"; public static const READY:String = "videotube-ready";
private static const DEBUG_VIDEO:Boolean = false;
public function fready():Boolean { throw "not implemented"; } 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 pause():void { throw "not implemented"; }
public function resume():void { throw "not implemented"; } public function resume():void { throw "not implemented"; }
public function time():Number { throw "not implemented"; } public function time():Number { throw "not implemented"; }
@ -21,84 +24,132 @@ package
private var secPrev:Number; private var secPrev:Number;
private var iqte:int; private var iqte:int;
private var iqtePrev:int; private var qtePrev:Qte;
protected var gamedisc:Gamedisc; protected var gamedisc:Gamedisc;
private var textDebug:TextField;
public function Videotube(gamedisc: Gamedisc) public function Videotube(gamedisc: Gamedisc)
{ {
this.gamedisc = gamedisc; this.gamedisc = gamedisc;
secPrev = 0; secPrev = 0;
iqte = 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 public function seek(sec:Number):void
{ {
trace("seeking");
seekI(sec); seekI(sec);
if (iqtePrev != -1) cancelPrev();
{
dispatchEvent(new EventQte(EventQte.QTE_CANCEL, gamedisc.rgqte[iqtePrev]));
iqtePrev = -1;
}
iqte = -1; iqte = -1;
trace("seek to " + sec + ":" + iqte);
} }
// returns true if triggered // 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; var secQte:Number;
if (ev == EventQte.QTE) if (ev == EventQte.QTE)
secQte = qte.secTrigger; secQte = qte.secTrigger();
else else
secQte = qte.secTimeout(); secQte = qte.secTimeout();
//trace("testing " + iqte + " for " + ev + " " + secQte + " in " + secPrev + ":" + secNow);
if (Util.FInTimespan(secQte, secPrev, secNow)) if (Util.FInTimespan(secQte, secPrev, secNow))
{ {
trace("triggered " + ev);
dispatchEvent(new EventQte(ev, qte)); dispatchEvent(new EventQte(ev, qte));
return true; return true;
} }
} }
return false; return false;
} }
protected function cancelPrev():void {
if (qtePrev != null) {
dispatchEvent(new EventQte(EventQte.QTE_CANCEL, qtePrev));
qtePrev = null;
}
}
protected function tick(e: Event):void protected function tick(e: Event):void
{ {
var secNow:Number = time(); var secNow:Number = time();
var fPrevQteProcessed:Boolean = false; var fPrevQteProcessed:Boolean = false;
var fQteProcessed:Boolean = false; var fQteProcessed:Boolean = false;
if (secNow < secPrev) {
iqte = -1;
cancelPrev();
secPrev = secNow;
}
if (iqte < 0) if (iqte < 0)
{ {
iqte = Util.binarySearch(gamedisc.rgqte, secNow, function(qte:Qte, secNow:Number):int { trace("finding qte for time " + secNow);
if (qte.secTrigger < secNow) iqte = Math.abs(Util.binarySearch(gamedisc.rgqte, secNow, function(qte:Qte, secNow:Number):int {
if (qte.secTrigger() < secNow)
return -1; return -1;
if (qte.secTrigger > secNow) if (qte.secTrigger() > secNow)
return 1; return 1;
return 0; 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 else
{ {
// we loop here so that, in the event of being passed bad data, we still do something vaguely sensible. // we loop here so that, in the event of being passed bad data, we still do something vaguely sensible.
while (!fPrevQteProcessed && !fQteProcessed) 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; fPrevQteProcessed = false;
iqtePrev ++; qtePrev = null;
} }
else else
fPrevQteProcessed = true; 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; fQteProcessed = false;
iqte ++; iqte ++;
qtePrev = qte;
} }
else else
fQteProcessed = true; 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; 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); removeEventListener(Event.REMOVED_FROM_STAGE, cleanup);
stage.removeEventListener(Event.ENTER_FRAME, tick); stage.removeEventListener(Event.ENTER_FRAME, tick);
} }
public override function fready():Boolean { return true; } public override function fready():Boolean { return true; }
public override function play():void { stream.play(gamedisc.urlVideo); }
public override function pause():void { stream.pause(); } public override function pause():void { stream.pause(); }
public override function resume():void { stream.resume(); } public override function resume():void { stream.resume(); }
public override function time():Number { return stream.time; } 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); super(gamedisc);
Security.allowDomain("www.youtube.com"); Security.allowDomain("www.youtube.com");
Security.allowDomain("*.ytimage.com");
player = null; player = null;
loader = new Loader(); loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.INIT, onLoaderInit); loader.contentLoaderInfo.addEventListener(Event.INIT, onLoaderInit);
@ -23,12 +24,23 @@ package
} }
private function onLoaderInit(event:Event):void private function onLoaderInit(event:Event):void
{ {
trace("yt: loader init");
addChild(loader); addChild(loader);
loader.contentLoaderInfo.removeEventListener(Event.INIT, onLoaderInit); 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 private function onPlayerReady(event:Event):void
{ {
trace("yt: player ready");
player = loader.content; player = loader.content;
player.setSize(stage.stageWidth, stage.stageHeight); player.setSize(stage.stageWidth, stage.stageHeight);
player.cueVideoById(gamedisc.urlVideo); player.cueVideoById(gamedisc.urlVideo);
@ -36,11 +48,11 @@ package
stage.addEventListener(Event.ENTER_FRAME, tick); stage.addEventListener(Event.ENTER_FRAME, tick);
dispatchEvent(new Event(Videotube.READY)); dispatchEvent(new Event(Videotube.READY));
} }
public override function fready():Boolean { return player !== null; } public override function fready():Boolean { return player !== null && player.getPlayerState() >= 0; }
public override function play():void { player.playVideo(); } public override function enqueue():void { seek(0); }
public override function pause():void { player.pauseVideo(); } public override function pause():void { player.pauseVideo(); }
public override function resume():void { player.playVideo(); } public override function resume():void { player.playVideo(); }
public override function time():Number { return player.getCurrentTime(); } 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); }
} }
} }