More improvements to world streaming.

This commit is contained in:
Nick Winter 2014-08-21 21:23:45 -07:00
parent bd380e4a09
commit 9b31e28536
6 changed files with 72 additions and 101 deletions

View file

@ -367,29 +367,18 @@ self.runWorld = function runWorld(args) {
self.serializeFramesSoFar = function serializeFramesSoFar() { self.serializeFramesSoFar = function serializeFramesSoFar() {
if(!self.world) return console.error("hmm, no world when we went to serialize some frames?"); if(!self.world) return console.error("hmm, no world when we went to serialize some frames?");
var goalStates = self.goalManager.getGoalStates(); if(self.world.framesSerializedSoFar == self.world.frames.length) return;
var transferableSupported = self.transferableSupported(); self.onWorldLoaded();
var serialized = self.world.serializeFramesSoFar(); self.world.framesSerializedSoFar = self.world.frames.length;
if(!serialized) {
console.log("Tried to serialize some frames, but none have been simulated since last time; still at", self.world.framesSerializedSoFar);
return;
}
try {
var message = {type: 'some-frames-serialized', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
if(transferableSupported)
self.postMessage(message, serialized.transferableObjects);
else
self.postMessage(message);
}
catch(error) {
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
}
}; };
self.onWorldLoaded = function onWorldLoaded() { self.onWorldLoaded = function onWorldLoaded() {
self.goalManager.worldGenerationEnded(); if(self.world.framesSerializedSoFar == self.world.frames.length) return;
if(self.world.ended)
self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates(); var goalStates = self.goalManager.getGoalStates();
self.postMessage({type: 'end-load-frames', goalStates: goalStates}); if(self.world.ended)
self.postMessage({type: 'end-load-frames', goalStates: goalStates});
var t1 = new Date(); var t1 = new Date();
var diff = t1 - self.t0; var diff = t1 - self.t0;
if (self.world.headless) if (self.world.headless)
@ -402,10 +391,12 @@ self.onWorldLoaded = function onWorldLoaded() {
catch(error) { catch(error) {
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
} }
var t2 = new Date(); var t2 = new Date();
//console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
var messageType = self.world.ended ? 'new-world' : 'some-frames-serialized';
try { try {
var message = {type: 'new-world', serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame}; var message = {type: messageType, serialized: serialized.serializedWorld, goalStates: goalStates, startFrame: serialized.startFrame, endFrame: serialized.endFrame};
if(transferableSupported) if(transferableSupported)
self.postMessage(message, serialized.transferableObjects); self.postMessage(message, serialized.transferableObjects);
else else
@ -414,11 +405,14 @@ self.onWorldLoaded = function onWorldLoaded() {
catch(error) { catch(error) {
console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace); console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
} }
var t3 = new Date();
console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms"); if(self.world.ended) {
self.world.goalManager.destroy(); var t3 = new Date();
self.world.destroy(); console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms");
self.world = null; self.world.goalManager.destroy();
self.world.destroy();
self.world = null;
}
}; };
self.onWorldError = function onWorldError(error) { self.onWorldError = function onWorldError(error) {

View file

@ -67,7 +67,7 @@ module.exports = class Angel extends CocoClass
# We pay attention to certain progress indicators as the world loads. # We pay attention to certain progress indicators as the world loads.
when 'world-load-progress-changed' when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializingStreamingFrames or @shared.firstWorld
@worker.postMessage func: 'serializeFramesSoFar' # Stream it! @worker.postMessage func: 'serializeFramesSoFar' # Stream it!
when 'console-log' when 'console-log'
@log event.data.args... @log event.data.args...
@ -84,14 +84,14 @@ module.exports = class Angel extends CocoClass
# We have some of the frames serialized, so let's send the partially simulated world to the Surface. # We have some of the frames serialized, so let's send the partially simulated world to the Surface.
when 'some-frames-serialized' when 'some-frames-serialized'
console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld #console.log "angel received some frames", event.data.serialized, "with goals", event.data.goalStates, "and streaming into world", @shared.streamingWorld
@deserializingStreamingFrames = true @deserializingStreamingFrames = true
@beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
# Either the world finished simulating successfully, or we abort the worker. # Either the world finished simulating successfully, or we abort the worker.
when 'new-world' when 'new-world'
console.log "angel received alll frames", event.data.serialized #console.log "angel received alll frames", event.data.serialized, "and have streaming world", @shared.streamingWorld
@beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame @beholdWorld event.data.serialized, event.data.goalStates, event.data.startFrame, event.data.endFrame, @shared.streamingWorld
when 'abort' when 'abort'
@say 'Aborted.', event.data @say 'Aborted.', event.data
clearTimeout @abortTimeout clearTimeout @abortTimeout
@ -118,24 +118,25 @@ module.exports = class Angel extends CocoClass
finishBeholdingWorld: (goalStates) -> (world) => finishBeholdingWorld: (goalStates) -> (world) =>
return if @aborting return if @aborting
@shared.streamingWorld = world
finished = world.frames.length is world.totalFrames finished = world.frames.length is world.totalFrames
firstChangedFrame = world.findFirstChangedFrame @shared.world
eventType = if finished then 'god:new-world-created' else 'god:streaming-world-updated'
if finished if finished
world.findFirstChangedFrame @shared.world
@shared.world = world @shared.world = world
Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team if @shared.firstWorld Backbone.Mediator.publish eventType, world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team, firstChangedFrame: firstChangedFrame
if finished
for scriptNote in @shared.world.scriptNotes for scriptNote in @shared.world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@shared.goalManager?.world = world @shared.goalManager?.world = world
@finishWork() @finishWork()
else else
@shared.streamingWorld = world
#Backbone.Mediator.publish 'god:new-world-created', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
Backbone.Mediator.publish 'god:streaming-world-updated', world: world, firstWorld: @shared.firstWorld, goalStates: goalStates, team: me.team
@deserializingStreamingFrames = false @deserializingStreamingFrames = false
finishWork: -> finishWork: ->
@shared.streamingWorld = null @shared.streamingWorld = null
@shared.firstWorld = false @shared.firstWorld = false
@deserializingStreamingFrames = false
@running = false @running = false
_.remove @shared.busyAngels, @ _.remove @shared.busyAngels, @
@doWork() @doWork()

View file

@ -205,10 +205,11 @@ module.exports = Surface = class Surface extends CocoClass
@scrubbingTo = Math.min(Math.round(progress * @world.totalFrames), @world.totalFrames) @scrubbingTo = Math.min(Math.round(progress * @world.totalFrames), @world.totalFrames)
@scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5)) @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
ease = if @fastForwarding then createjs.Ease.linear else createjs.Ease.sineInOut
if scrubDuration if scrubDuration
t = createjs.Tween t = createjs.Tween
.get(@) .get(@)
.to({currentFrame: @scrubbingTo}, scrubDuration, createjs.Ease.sineInOut) .to({currentFrame: @scrubbingTo}, scrubDuration, ease)
.call(onTweenEnd) .call(onTweenEnd)
t.addEventListener('change', @onFramesScrubbed) t.addEventListener('change', @onFramesScrubbed)
else else
@ -354,48 +355,36 @@ module.exports = Surface = class Surface extends CocoClass
return if e.preload return if e.preload
@setPaused false if @ended @setPaused false if @ended
@casting = true @casting = true
@wasPlayingWhenCastingBegan = @playing @setPlayingCalled = false # Don't overwrite playing settings if they changed by, say, scripts.
Backbone.Mediator.publish 'level-set-playing', {playing: false} @frameBeforeCast = @currentFrame
@setPlayingCalled = false # don't overwrite playing settings if they changed by, say, scripts @currentFrame = 0
if @coordinateDisplay?
@surfaceTextLayer.removeChild @coordinateDisplay
@coordinateDisplay.destroy()
createjs.Tween.removeTweens(@surfaceLayer)
createjs.Tween.get(@surfaceLayer).to({alpha: 0.9}, 1000, createjs.Ease.getPowOut(4.0))
onNewWorld: (event) -> onNewWorld: (event) ->
return unless event.world.name is @world.name return unless event.world.name is @world.name
@casting = false @casting = false
if @ended and not @wasPlayingWhenCastingBegan @spriteBoss.play()
@setPaused true
else
@spriteBoss.play()
# This has a tendency to break scripts that are waiting for playback to change when the level is loaded # This has a tendency to break scripts that are waiting for playback to change when the level is loaded
# so only run it after the first world is created. # so only run it after the first world is created.
Backbone.Mediator.publish 'level-set-playing', {playing: @wasPlayingWhenCastingBegan} unless event.firstWorld or @setPlayingCalled Backbone.Mediator.publish 'level-set-playing', {playing: true} unless event.firstWorld or @setPlayingCalled
fastForwardTo = null @setWorld event.world
if @playing @onFrameChanged(true)
fastForwardTo = Math.min event.world.firstChangedFrame, @currentFrame if @playing and ffToFrame = Math.min event.firstChangedFrame, @frameBeforeCast, event.world.frames.length
@currentFrame = 0 ffToRatio = ffToFrame / @world.totalFrames
ffToTime = ffToFrame * @world.dt
createjs.Tween.removeTweens(@surfaceLayer) ffSpeed = Math.max 4, ffToTime / 3
f = => ffInterval = 1000 * (ffToFrame - @currentFrame) / @options.frameRate
@setWorld event.world ffScrubDuration = 1000 * ffToTime / ffSpeed
@onFrameChanged(true) ffScrubDuration = Math.min(ffScrubDuration, ffInterval)
if fastForwardTo and @playing ffFactor = ffInterval / ffScrubDuration
fastForwardToRatio = fastForwardTo / @world.totalFrames if ffFactor > 2
fastForwardToTime = fastForwardTo * @world.dt createjs.Tween.removeTweens(@)
fastForwardSpeed = Math.max 4, fastForwardToTime / 3 @scrubbingTo = null
@setProgress fastForwardToRatio, 1000 * fastForwardToTime / fastForwardSpeed
@fastForwarding = true @fastForwarding = true
createjs.Tween.get(@surfaceLayer) @setProgress ffToRatio, ffScrubDuration
.to({alpha: 0.0}, 50) else
.call(f) createjs.Tween.removeTweens(@)
.to({alpha: 1.0}, 2000, createjs.Ease.getPowOut(2.0))
# initialization # initialization
@ -414,7 +403,7 @@ module.exports = Surface = class Surface extends CocoClass
@surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds @surfaceLayer.addChild @cameraBorder = new CameraBorder bounds: @camera.bounds
@screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight @screenLayer.addChild new Letterbox canvasWidth: canvasWidth, canvasHeight: canvasHeight
@spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible @spriteBoss = new SpriteBoss camera: @camera, surfaceLayer: @surfaceLayer, surfaceTextLayer: @surfaceTextLayer, world: @world, thangTypes: @options.thangTypes, choosing: @options.choosing, navigateToSelection: @options.navigateToSelection, showInvisible: @options.showInvisible
#@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer # Not needed with world streaming. #@castingScreen ?= new CastingScreen camera: @camera, layer: @screenLayer # STREAM: Not needed with world streaming.
@playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer @playbackOverScreen ?= new PlaybackOverScreen camera: @camera, layer: @screenLayer
@stage.enableMouseOver(10) @stage.enableMouseOver(10)
@stage.addEventListener 'stagemousemove', @onMouseMove @stage.addEventListener 'stagemousemove', @onMouseMove
@ -616,7 +605,7 @@ module.exports = Surface = class Surface extends CocoClass
updateState: (frameChanged) -> updateState: (frameChanged) ->
# world state must have been restored in @restoreWorldState # world state must have been restored in @restoreWorldState
@camera.updateZoom() @camera.updateZoom()
@spriteBoss.update frameChanged unless @casting @spriteBoss.update frameChanged
@dimmer?.setSprites @spriteBoss.sprites @dimmer?.setSprites @spriteBoss.sprites
drawCurrentFrame: (e) -> drawCurrentFrame: (e) ->

View file

@ -47,6 +47,7 @@ module.exports = class ThangState
value = @specialKeysToValues[specialKey] value = @specialKeysToValues[specialKey]
else if type is 'Thang' else if type is 'Thang'
specialKey = storage[@frameIndex] specialKey = storage[@frameIndex]
console.error "couldn't find world from thang", @thang, "for", @specialKeysToValues[specialKey] unless @thang.world
value = @thang.world.getThangByID @specialKeysToValues[specialKey] value = @thang.world.getThangByID @specialKeysToValues[specialKey]
else if type is 'array' else if type is 'array'
specialKey = storage[@frameIndex] specialKey = storage[@frameIndex]
@ -164,6 +165,7 @@ module.exports = class ThangState
# Optimize like no tomorrow--most performance-sensitive part of the whole app, called once per WorldFrame per Thang per trackedProperty, blocking the UI # Optimize like no tomorrow--most performance-sensitive part of the whole app, called once per WorldFrame per Thang per trackedProperty, blocking the UI
ts = new ThangState ts = new ThangState
ts.thang = thang ts.thang = thang
console.error "couldn't find thang!", @ unless thang
ts.frameIndex = frameIndex ts.frameIndex = frameIndex
ts.trackedPropertyKeys = trackedPropertyKeys ts.trackedPropertyKeys = trackedPropertyKeys
ts.trackedPropertyTypes = trackedPropertyTypes ts.trackedPropertyTypes = trackedPropertyTypes

View file

@ -27,7 +27,6 @@ module.exports = class World
# classMap is needed for deserializing Worlds, Thangs, and other classes # classMap is needed for deserializing Worlds, Thangs, and other classes
@classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment} @classMap = classMap ? {Vector: Vector, Rectangle: Rectangle, Thang: Thang, Ellipse: Ellipse, LineSegment: LineSegment}
Thang.resetThangIDs() Thang.resetThangIDs()
@aRandomID = Math.random()
@userCodeMap ?= {} @userCodeMap ?= {}
@thangs = [] @thangs = []
@ -288,16 +287,10 @@ module.exports = class World
addTrackedProperties: (props...) -> addTrackedProperties: (props...) ->
@trackedProperties = (@trackedProperties ? []).concat props @trackedProperties = (@trackedProperties ? []).concat props
serializeFramesSoFar: -> serialize: ->
return null if @frames.length is @framesSerializedSoFar
serialized = @serialize @framesSerializedSoFar, @frames.length
@framesSerializedSoFar = @frames.length
serialized
serialize: (startFrame=0, endFrame=null) ->
# Code hotspot; optimize it # Code hotspot; optimize it
if not endFrame? and @frames.length < @totalFrames then throw new Error('World Should Be Over Before Serialization') startFrame = @framesSerializedSoFar
endFrame ?= @totalFrames endFrame = @frames.length
console.log "... world serializing frames from", startFrame, "to", endFrame console.log "... world serializing frames from", startFrame, "to", endFrame
[transferableObjects, nontransferableObjects] = [0, 0] [transferableObjects, nontransferableObjects] = [0, 0]
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}} o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}}
@ -321,7 +314,7 @@ module.exports = class World
for thang in @thangs for thang in @thangs
# Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles). # Don't serialize empty trackedProperties for stateless Thangs which haven't changed (like obstacles).
# Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then). # Check both, since sometimes people mark stateless Thangs but don't change them, and those should still be tracked, and the inverse doesn't work on the other end (we'll just think it doesn't exist then).
continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean)# and not streaming continue if thang.stateless and not _.some(thang.trackedPropertiesUsed, Boolean) and not streaming
o.trackedPropertiesThangIDs.push thang.id o.trackedPropertiesThangIDs.push thang.id
trackedPropertiesIndices = [] trackedPropertiesIndices = []
trackedPropertiesKeys = [] trackedPropertiesKeys = []
@ -401,8 +394,8 @@ module.exports = class World
perf.t1 = now() perf.t1 = now()
if w.thangs.length if w.thangs.length
for thang in o.thangs when not w.thangMap[thang.id] for thangConfig in o.thangs when not w.thangMap[thangConfig.id]
w.thangs.push Thang.deserialize(thang, w, classMap) w.thangs.push thang = Thang.deserialize(thangConfig, w, classMap)
w.setThang thang w.setThang thang
else else
w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs) w.thangs = (Thang.deserialize(thang, w, classMap) for thang in o.thangs)
@ -458,16 +451,16 @@ module.exports = class World
finishedWorldCallback w finishedWorldCallback w
findFirstChangedFrame: (oldWorld) -> findFirstChangedFrame: (oldWorld) ->
return @firstChangedFrame = 0 unless oldWorld return 0 unless oldWorld
for newFrame, i in @frames for newFrame, i in @frames
oldFrame = oldWorld.frames[i] oldFrame = oldWorld.frames[i]
break unless oldFrame and newFrame.hash is oldFrame.hash break unless oldFrame and ((newFrame.hash is oldFrame.hash) or not newFrame.hash? or not oldFrame.hash?) # undefined gets in there when streaming at the last frame of each batch for some reason
@firstChangedFrame = i firstChangedFrame = i
if @frames[i] if @frames[i]
console.log 'First changed frame is', @firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash console.log 'First changed frame is', firstChangedFrame, 'with hash', @frames[i].hash, 'compared to', oldWorld.frames[i]?.hash
else else
console.log 'No frames were changed out of all', @frames.length console.log 'No frames were changed out of all', @frames.length
@firstChangedFrame firstChangedFrame
pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) -> pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) ->
# Optimized # Optimized

View file

@ -24,7 +24,6 @@ module.exports = class LevelPlaybackView extends CocoView
'god:new-world-created': 'onNewWorld' 'god:new-world-created': 'onNewWorld'
'god:streaming-world-updated': 'onNewWorld' # Maybe? 'god:streaming-world-updated': 'onNewWorld' # Maybe?
'level-set-letterbox': 'onSetLetterbox' 'level-set-letterbox': 'onSetLetterbox'
'tome:cast-spells': 'onCastSpells'
events: events:
'click #debug-toggle': 'onToggleDebug' 'click #debug-toggle': 'onToggleDebug'
@ -151,10 +150,10 @@ module.exports = class LevelPlaybackView extends CocoView
@barWidth = $('.progress', @$el).width() @barWidth = $('.progress', @$el).width()
onNewWorld: (e) -> onNewWorld: (e) ->
@totalTime = e.world.totalFrames / e.world.frameRate @totalLoadedTime = e.world.frames.length * e.world.dt
pct = parseInt(100 * e.world.totalFrames / e.world.maxTotalFrames) + '%' @totalTime = e.world.totalFrames * e.world.dt
pct = parseInt(100 * e.world.frames.length / e.world.maxTotalFrames) + '%'
@barWidth = $('.progress', @$el).css('width', pct).show().width() @barWidth = $('.progress', @$el).css('width', pct).show().width()
@casting = false
$('.scrubber .progress', @$el).slider('enable', true) $('.scrubber .progress', @$el).slider('enable', true)
@newTime = 0 @newTime = 0
@currentTime = 0 @currentTime = 0
@ -189,11 +188,6 @@ module.exports = class LevelPlaybackView extends CocoView
onViewKeyboardShortcuts: -> onViewKeyboardShortcuts: ->
@openModalView new KeyboardShortcutsModal() @openModalView new KeyboardShortcutsModal()
onCastSpells: (e) ->
return if e.preload
@casting = true
@$progressScrubber.slider('disable', true)
onDisableControls: (e) -> onDisableControls: (e) ->
if not e.controls or 'playback' in e.controls if not e.controls or 'playback' in e.controls
@disabled = true @disabled = true
@ -279,7 +273,7 @@ module.exports = class LevelPlaybackView extends CocoView
@timePopup.show() @timePopup.show()
updateProgress: (progress) -> updateProgress: (progress) ->
$('.scrubber .progress-bar', @$el).css('width', "#{progress*100}%") $('.scrubber .progress-bar', @$el).css('width', "#{progress * 100 * @totalTime / @totalLoadedTime}%")
updatePlayButton: (progress) -> updatePlayButton: (progress) ->
if progress >= 0.99 and @lastProgress < 0.99 if progress >= 0.99 and @lastProgress < 0.99
@ -330,9 +324,7 @@ module.exports = class LevelPlaybackView extends CocoView
return if @shouldIgnore() return if @shouldIgnore()
Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration Backbone.Mediator.publish 'level-set-time', ratio: ratio, scrubDuration: duration
shouldIgnore: -> shouldIgnore: -> return @disabled
#return @disabled or @casting or false # STREAM: figure this out
return false
onTogglePlay: (e) -> onTogglePlay: (e) ->
e?.preventDefault() e?.preventDefault()