Bug 962736 - Update Shumway to version 0.8.6. r=till
authorYury Delendik <ydelendik@mozilla.com>
Fri, 24 Jan 2014 09:26:32 -0600
changeset 165396 4f4f4b20efc776df69a6e6b07002746f6141c558
parent 165395 a3451e7f4856abc0ed4fea891ff0f74a6a6e60c7
child 165397 ecd9c9d606985e816cc18a5d36b075a424d7f002
push id26089
push userryanvm@gmail.com
push dateMon, 27 Jan 2014 20:23:17 +0000
treeherdermozilla-central@ba31f271fbd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstill
bugs962736
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 962736 - Update Shumway to version 0.8.6. r=till
browser/extensions/shumway/content/ShumwayStreamConverter.jsm
browser/extensions/shumway/content/shumway-worker.js
browser/extensions/shumway/content/shumway.js
browser/extensions/shumway/content/version.txt
browser/extensions/shumway/content/web/avm-sandbox.js
browser/extensions/shumway/content/web/viewer.html
--- a/browser/extensions/shumway/content/ShumwayStreamConverter.jsm
+++ b/browser/extensions/shumway/content/ShumwayStreamConverter.jsm
@@ -32,28 +32,34 @@ const EXPECTED_PLAYPREVIEW_URI_PREFIX = 
 const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
 const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
 
 const MAX_CLIPBOARD_DATA_SIZE = 8000;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/NetUtil.jsm');
+Cu.import('resource://gre/modules/Promise.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
   'resource://gre/modules/PrivateBrowsingUtils.jsm');
 
+XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager',
+  'resource://gre/modules/AddonManager.jsm');
+
 XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
   'resource://shumway/ShumwayTelemetry.jsm');
 
-let appInfo = Cc['@mozilla.org/xre/app-info;1'].getService(Ci.nsIXULAppInfo);
 let Svc = {};
 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
                                    '@mozilla.org/mime;1', 'nsIMIMEService');
 
+let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"];
+let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"];
+
 function getBoolPref(pref, def) {
   try {
     return Services.prefs.getBoolPref(pref);
   } catch (ex) {
     return def;
   }
 }
 
@@ -183,16 +189,42 @@ function isShumwayEnabledFor(actions) {
     /sndcdn\.com\/assets\/swf/.test(url) /* soundcloud */ ||
     /vimeocdn\.com/.test(url) /* vimeo */) {
     return false;
   }
 
   return true;
 }
 
+function getVersionInfo() {
+  var deferred = Promise.defer();
+  var versionInfo = {
+    geckoMstone : 'unknown',
+    geckoBuildID: 'unknown',
+    shumwayVersion: 'unknown'
+  };
+  try {
+    versionInfo.geckoMstone = Services.prefs.getCharPref('gecko.mstone');
+    versionInfo.geckoBuildID = Services.prefs.getCharPref('gecko.buildID');
+  } catch (e) {
+    log('Error encountered while getting platform version info:', e);
+  }
+  try {
+    var addonId = "shumway@research.mozilla.org";
+    AddonManager.getAddonByID(addonId, function(addon) {
+      versionInfo.shumwayVersion = addon ? addon.version : 'n/a';
+      deferred.resolve(versionInfo);
+    });
+  } catch (e) {
+    log('Error encountered while getting Shumway version info:', e);
+    deferred.resolve(versionInfo);
+  }
+  return deferred.promise;
+}
+
 function fallbackToNativePlugin(window, userAction, activateCTP) {
   var obj = window.frameElement;
   var doc = obj.ownerDocument;
   var e = doc.createEvent("CustomEvent");
   e.initCustomEvent("MozPlayPlugin", true, true, activateCTP);
   obj.dispatchEvent(e);
 
   ShumwayTelemetry.onFallback(userAction);
@@ -361,17 +393,17 @@ ChromeActions.prototype = {
       } else {
         log("data access id prohibited to " + url + " from " + baseUrl);
         win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
           error: "only original swf file or file from the same origin loading supported"}, "*");
       }
     });
   },
   fallback: function(automatic) {
-    automatic = !!automatic; // cast to boolean
+    automatic = !!automatic;
     fallbackToNativePlugin(this.window, !automatic, automatic);
   },
   setClipboard: function (data) {
     if (typeof data !== 'string' ||
         data.length > MAX_CLIPBOARD_DATA_SIZE ||
         !this.document.hasFocus()) {
       return;
     }
@@ -427,16 +459,38 @@ ChromeActions.prototype = {
       if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE &&
           !this.telemetry.errors[errorType]) {
         this.telemetry.errors[errorType] = true; // record only one report per SWF
         ShumwayTelemetry.onError(errorType);
       }
       break;
     }
   },
+  reportIssue: function(exceptions) {
+    var base = "http://shumway-issue-reporter.paas.allizom.org/input?";
+    var windowUrl = this.window.parent.wrappedJSObject.location + '';
+    var params = 'url=' + encodeURIComponent(windowUrl);
+    params += '&swf=' + encodeURIComponent(this.url);
+    getVersionInfo().then(function (versions) {
+      params += '&ffbuild=' + encodeURIComponent(versions.geckoMstone + ' (' +
+                                                 versions.geckoBuildID + ')');
+      params += '&shubuild=' + encodeURIComponent(versions.shumwayVersion);
+    }).then(function () {
+      var postDataStream = StringInputStream.
+                           createInstance(Ci.nsIStringInputStream);
+      postDataStream.data = 'exceptions=' + encodeURIComponent(exceptions);
+      var postData = MimeInputStream.createInstance(Ci.nsIMIMEInputStream);
+      postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
+      postData.addContentLength = true;
+      postData.setData(postDataStream);
+      this.window.openDialog('chrome://browser/content', '_blank',
+                             'all,dialog=no', base + params, null, null,
+                             postData);
+    }.bind(this));
+  },
   externalCom: function (data) {
     if (!this.allowScriptAccess)
       return;
 
     // TODO check security ?
     var parentWindow = this.window.parent.wrappedJSObject;
     var embedTag = this.embedTag.wrappedJSObject;
     switch (data.action) {
@@ -454,16 +508,19 @@ ChromeActions.prototype = {
       return parentWindow.__flash__eval(data.expression);
     case 'call':
       return parentWindow.__flash__call(data.request);
     case 'register':
       return embedTag.__flash__registerCallback(data.functionName);
     case 'unregister':
       return embedTag.__flash__unregisterCallback(data.functionName);
     }
+  },
+  getWindowUrl: function() {
+    return this.window.parent.wrappedJSObject.location + '';
   }
 };
 
 // Event listener to trigger chrome privedged code.
 function RequestListener(actions) {
   this.actions = actions;
 }
 // Receive an event and synchronously or asynchronously responds.
--- a/browser/extensions/shumway/content/shumway-worker.js
+++ b/browser/extensions/shumway/content/shumway-worker.js
@@ -3870,16 +3870,23 @@ function createParsingContext(commitData
           height: (bbox.yMax - bbox.yMin) / 20,
           isAvm2: !(!result.fileAttributes.doAbc)
         };
       }
       commitData({
         command: 'complete',
         stats: stats
       });
+    },
+    onexception: function (e) {
+      commitData({
+        type: 'exception',
+        message: e.message,
+        stack: e.stack
+      });
     }
   };
 }
 function parseBytes(bytes, commitData) {
   SWF.parse(bytes, createParsingContext(commitData));
 }
 function ResourceLoader(scope) {
   this.subscription = null;
@@ -5808,17 +5815,17 @@ var readHeader = function readHeader($by
   $.frameRate = readUi8($bytes, $stream) + frameRateFraction / 256;
   $.frameCount = readUi16($bytes, $stream);
   return $;
 };
 (function (global) {
   global['tagHandler'] = tagHandler;
   global['readHeader'] = readHeader;
 }(this));
-function readTags(context, stream, swfVersion, final, onprogress) {
+function readTags(context, stream, swfVersion, final, onprogress, onexception) {
   var tags = context.tags;
   var bytes = stream.bytes;
   var lastSuccessfulPosition;
   var tag = null;
   if (context._readTag) {
     tag = context._readTag;
     delete context._readTag;
   }
@@ -5877,16 +5884,17 @@ function readTags(context, stream, swfVe
       if (onprogress) {
         onprogress(context);
       }
     } else {
       context._readTag = tag;
     }
   } catch (e) {
     if (e !== StreamNoDataError) {
+      onexception && onexception(e);
       throw e;
     }
     stream.pos = lastSuccessfulPosition;
     context._readTag = tag;
   }
 }
 function HeadTailBuffer(defaultSize) {
   this.bufferSize = defaultSize || 16;
@@ -6036,17 +6044,17 @@ BodyParser.prototype = {
     }
     var finalBlock = false;
     if (progressInfo) {
       swf.bytesLoaded = progressInfo.bytesLoaded;
       swf.bytesTotal = progressInfo.bytesTotal;
       finalBlock = progressInfo.bytesLoaded >= progressInfo.bytesTotal;
     }
     var readStartTime = performance.now();
-    readTags(swf, stream, swfVersion, finalBlock, options.onprogress);
+    readTags(swf, stream, swfVersion, finalBlock, options.onprogress, options.onexception);
     swf.parseTime += performance.now() - readStartTime;
     var read = stream.pos;
     buffer.removeHead(read);
     this.totalRead += read;
     if (options.oncomplete && swf.tags[swf.tags.length - 1].finalTag) {
       options.oncomplete(swf);
     }
   }
--- a/browser/extensions/shumway/content/shumway.js
+++ b/browser/extensions/shumway/content/shumway.js
@@ -5016,33 +5016,20 @@ var disableRenderVisitor = rendererOptio
 var disableMouseVisitor = rendererOptions.register(new Option('dmv', 'disableMouseVisitor', 'boolean', false, 'disable mouse visitor'));
 var showRedrawRegions = rendererOptions.register(new Option('rr', 'showRedrawRegions', 'boolean', false, 'show redraw regions'));
 var renderAsWireframe = rendererOptions.register(new Option('raw', 'renderAsWireframe', 'boolean', false, 'render as wireframe'));
 var showQuadTree = rendererOptions.register(new Option('qt', 'showQuadTree', 'boolean', false, 'show quad tree'));
 var turboMode = rendererOptions.register(new Option('', 'turbo', 'boolean', false, 'turbo mode'));
 var forceHidpi = rendererOptions.register(new Option('', 'forceHidpi', 'boolean', false, 'force hidpi'));
 var skipFrameDraw = rendererOptions.register(new Option('', 'skipFrameDraw', 'boolean', true, 'skip frame when not on time'));
 var hud = rendererOptions.register(new Option('', 'hud', 'boolean', false, 'show hud mode'));
+var dummyAnimation = rendererOptions.register(new Option('', 'dummy', 'boolean', false, 'show test balls animation'));
 var enableConstructChildren = rendererOptions.register(new Option('', 'constructChildren', 'boolean', true, 'Construct Children'));
 var enableEnterFrame = rendererOptions.register(new Option('', 'enterFrame', 'boolean', true, 'Enter Frame'));
 var enableAdvanceFrame = rendererOptions.register(new Option('', 'advanceFrame', 'boolean', true, 'Advance Frame'));
-if (typeof FirefoxCom !== 'undefined') {
-  turboMode.value = FirefoxCom.requestSync('getBoolPref', {
-    pref: 'shumway.turboMode',
-    def: false
-  });
-  hud.value = FirefoxCom.requestSync('getBoolPref', {
-    pref: 'shumway.hud',
-    def: false
-  });
-  forceHidpi.value = FirefoxCom.requestSync('getBoolPref', {
-    pref: 'shumway.force_hidpi',
-    def: false
-  });
-}
 var CanvasCache = {
     cache: [],
     getCanvas: function getCanvas(protoCanvas) {
       var tempCanvas = this.cache.shift();
       if (!tempCanvas) {
         tempCanvas = {
           canvas: document.createElement('canvas')
         };
@@ -5428,23 +5415,17 @@ function RenderingContext(refreshStage, 
   this.invalidPath = invalidPath;
   this.isClippingMask = false;
   this.colorTransform = new RenderingColorTransform();
   this.parentCtxs = [];
 }
 function renderDisplayObject(child, ctx, context) {
   var m = child._currentTransform;
   if (m) {
-    if (m.a * m.d == m.b * m.c) {
-      ctx.closePath();
-      ctx.rect(0, 0, 0, 0);
-      ctx.clip();
-    } else {
-      ctx.transform(m.a, m.b, m.c, m.d, m.tx / 20, m.ty / 20);
-    }
+    ctx.transform(m.a, m.b, m.c, m.d, m.tx / 20, m.ty / 20);
   }
   if (!renderAsWireframe.value) {
     if (child._alpha !== 1) {
       ctx.globalAlpha *= child._alpha;
     }
     if (context.invalidPath && !child._invalid && !context.refreshStage) {
       return;
     }
@@ -5541,21 +5522,67 @@ function initializeHUD(stage, parentCanv
   var canvasContainer = document.createElement('div');
   canvasContainer.appendChild(canvas);
   canvasContainer.style.position = 'absolute';
   canvasContainer.style.top = '0px';
   canvasContainer.style.left = '0px';
   canvasContainer.style.width = '100%';
   canvasContainer.style.height = '150px';
   canvasContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
+  canvasContainer.style.pointerEvents = 'none';
   parentCanvas.parentElement.appendChild(canvasContainer);
   hudTimeline = new Timeline(canvas);
   hudTimeline.setFrameRate(stage._frameRate);
   hudTimeline.refreshEvery(10);
 }
+function createRenderDummyBalls(ctx, stage) {
+  var dummyBalls;
+  var radius = 10;
+  var speed = 1;
+  var m = stage._concatenatedTransform;
+  var scaleX = m.a, scaleY = m.d;
+  dummyBalls = [];
+  for (var i = 0; i < 10; i++) {
+    dummyBalls.push({
+      position: {
+        x: radius + Math.random() * ((ctx.canvas.width - 2 * radius) / scaleX),
+        y: radius + Math.random() * ((ctx.canvas.height - 2 * radius) / scaleY)
+      },
+      velocity: {
+        x: speed * (Math.random() - 0.5),
+        y: speed * (Math.random() - 0.5)
+      }
+    });
+  }
+  ctx.fillStyle = 'black';
+  ctx.lineWidth = 2;
+  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+  return function renderDummyBalls() {
+    ctx.fillStyle = 'black';
+    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+    ctx.strokeStyle = 'green';
+    dummyBalls.forEach(function (ball) {
+      var position = ball.position;
+      var velocity = ball.velocity;
+      ctx.beginPath();
+      ctx.arc(position.x, position.y, radius, 0, Math.PI * 2, true);
+      ctx.stroke();
+      var x = position.x + velocity.x;
+      var y = position.y + velocity.y;
+      if (x < radius || x > ctx.canvas.width / scaleX - radius) {
+        velocity.x *= -1;
+      }
+      if (y < radius || y > ctx.canvas.height / scaleY - radius) {
+        velocity.y *= -1;
+      }
+      position.x += velocity.x;
+      position.y += velocity.y;
+    });
+  };
+}
 function renderStage(stage, ctx, events) {
   var frameWidth, frameHeight;
   if (!timeline && hud.value) {
     initializeHUD(stage, ctx.canvas);
   }
   function updateRenderTransform() {
     frameWidth = ctx.canvas.width;
     frameHeight = ctx.canvas.height;
@@ -5603,78 +5630,27 @@ function renderStage(stage, ctx, events)
     ctx.setTransform(scaleX, 0, 0, scaleY, offsetX, offsetY);
     var m = stage._concatenatedTransform;
     m.a = scaleX;
     m.d = scaleY;
     m.tx = offsetX * 20;
     m.ty = offsetY * 20;
   }
   updateRenderTransform();
-  var frameTime = 0;
-  var maxDelay = 1000 / stage._frameRate;
-  var nextRenderAt = performance.now();
+  var frameScheduler = new FrameScheduler();
+  stage._frameScheduler = frameScheduler;
   var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || window.setTimeout;
-  var renderDummyBalls;
-  var dummyBalls;
-  if (typeof FirefoxCom !== 'undefined' && FirefoxCom.requestSync('getBoolPref', {
-      pref: 'shumway.dummyMode',
-      def: false
-    })) {
-    var radius = 10;
-    var speed = 1;
-    var m = stage._concatenatedTransform;
-    var scaleX = m.a, scaleY = m.d;
-    dummyBalls = [];
-    for (var i = 0; i < 10; i++) {
-      dummyBalls.push({
-        position: {
-          x: radius + Math.random() * ((ctx.canvas.width - 2 * radius) / scaleX),
-          y: radius + Math.random() * ((ctx.canvas.height - 2 * radius) / scaleY)
-        },
-        velocity: {
-          x: speed * (Math.random() - 0.5),
-          y: speed * (Math.random() - 0.5)
-        }
-      });
-    }
-    ctx.fillStyle = 'black';
-    ctx.lineWidth = 2;
-    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-    renderDummyBalls = function () {
-      ctx.fillStyle = 'black';
-      ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-      ctx.strokeStyle = 'green';
-      dummyBalls.forEach(function (ball) {
-        var position = ball.position;
-        var velocity = ball.velocity;
-        ctx.beginPath();
-        ctx.arc(position.x, position.y, radius, 0, Math.PI * 2, true);
-        ctx.stroke();
-        var x = position.x + velocity.x;
-        var y = position.y + velocity.y;
-        if (x < radius || x > ctx.canvas.width / scaleX - radius) {
-          velocity.x *= -1;
-        }
-        if (y < radius || y > ctx.canvas.height / scaleY - radius) {
-          velocity.y *= -1;
-        }
-        position.x += velocity.x;
-        position.y += velocity.y;
-      });
-    };
-  }
+  var renderDummyBalls = dummyAnimation.value && createRenderDummyBalls(ctx, stage);
   console.timeEnd('Initialize Renderer');
   console.timeEnd('Total');
   var firstRun = true;
   var frameCount = 0;
   var frameFPSAverage = new metrics.Average(120);
-  function drawFrame(renderFrame, frameRequested) {
-    if (!skipFrameDraw.value) {
-      frameRequested = true;
-    }
+  var frameRequested = true;
+  function drawFrame(renderFrame, repaint) {
     sampleStart();
     var refreshStage = false;
     if (stage._invalid) {
       updateRenderTransform();
       stage._invalid = false;
       refreshStage = true;
     }
     var mouseMoved = false;
@@ -5703,17 +5679,24 @@ function renderStage(stage, ctx, events)
         timelineWrapBroadcastMessage(domain, 'executeFrame');
         timelineWrapBroadcastMessage(domain, 'exitFrame');
         timelineLeave('events');
       }
       if (stage._deferRenderEvent) {
         stage._deferRenderEvent = false;
         domain.broadcastMessage('render', 'render');
       }
-      if (isCanvasVisible(ctx.canvas) && (refreshStage || renderFrame) && frameRequested) {
+      var drawEnabled = isCanvasVisible(ctx.canvas) && (refreshStage || renderFrame) && (frameRequested || repaint || !skipFrameDraw.value);
+      if (drawEnabled && !repaint && skipFrameDraw.value && frameScheduler.shallSkipDraw) {
+        drawEnabled = false;
+        frameScheduler.skipDraw();
+        traceRenderer.value && appendToFrameTerminal('Skip Frame Draw', 'red');
+      }
+      if (drawEnabled) {
+        frameScheduler.startDraw();
         var invalidPath = null;
         traceRenderer.value && frameWriter.enter('> Invalidation');
         timelineEnter('invalidate');
         invalidPath = stage._processInvalidations(refreshStage);
         timelineLeave('invalidate');
         traceRenderer.value && frameWriter.leave('< Invalidation');
         if (!disableRenderVisitor.value && !invalidPath.isEmpty) {
           timelineEnter('render');
@@ -5726,16 +5709,17 @@ function renderStage(stage, ctx, events)
           ctx.strokeStyle = 'green';
           renderQuadTree(ctx, stage._qtree);
         }
         if (invalidPath && !refreshStage && showRedrawRegions.value) {
           ctx.strokeStyle = 'red';
           invalidPath.draw(ctx);
           ctx.stroke();
         }
+        frameScheduler.endDraw();
       }
       if (mouseMoved && !disableMouseVisitor.value) {
         renderFrame && timelineEnter('mouse');
         traceRenderer.value && frameWriter.enter('> Mouse Handling');
         stage._handleMouse();
         traceRenderer.value && frameWriter.leave('< Mouse Handling');
         renderFrame && timelineLeave('mouse');
         ctx.canvas.style.cursor = stage._cursor;
@@ -5752,74 +5736,154 @@ function renderStage(stage, ctx, events)
         traceRenderer.value && appendToFrameTerminal('End Frame Time: ' + frameElapsedTime.toFixed(2) + ' (' + frameFPS.toFixed(2) + ' fps, ' + frameFPSAverage.average().toFixed(2) + ' average fps)', 'purple');
       }
       timelineLeave('frame');
     } else {
       traceRenderer.value && appendToFrameTerminal('Skip Frame', 'black');
     }
     sampleEnd();
   }
-  var frameRequested = true;
-  var skipNextFrameDraw = false;
   (function draw() {
-    var now = performance.now();
     var renderFrame = true;
     if (events.onBeforeFrame) {
       var e = {
           cancel: false
         };
       events.onBeforeFrame(e);
       renderFrame = !e.cancel;
     }
-    frameTime = now;
-    if (renderFrame && renderDummyBalls) {
-      renderDummyBalls();
+    if (renderDummyBalls) {
+      if (renderFrame) {
+        renderDummyBalls();
+        events.onAfterFrame && events.onAfterFrame();
+      }
+      setTimeout(draw);
       return;
     }
-    drawFrame(renderFrame, frameRequested && !skipNextFrameDraw);
+    frameScheduler.startFrame(stage._frameRate);
+    drawFrame(renderFrame, false);
+    frameScheduler.endFrame();
     frameRequested = false;
-    maxDelay = 1000 / stage._frameRate;
-    if (!turboMode.value) {
-      nextRenderAt += maxDelay;
-      var wasLate = false;
-      while (nextRenderAt < now) {
-        wasLate = true;
-        nextRenderAt += maxDelay;
-      }
-      if (wasLate && !skipNextFrameDraw) {
-        skipNextFrameDraw = true;
-        traceRenderer.value && appendToFrameTerminal('Skip Frame Draw', 'red');
-      } else {
-        skipNextFrameDraw = false;
-      }
-    } else {
-      nextRenderAt = now;
+    if (!frameScheduler.isOnTime) {
+      traceRenderer.value && appendToFrameTerminal('Frame Is Late', 'red');
     }
     if (renderFrame && events.onAfterFrame) {
       events.onAfterFrame();
     }
     if (renderingTerminated) {
       if (events.onTerminated) {
         events.onTerminated();
       }
       return;
     }
-    setTimeout(draw, Math.max(0, nextRenderAt - performance.now()));
+    setTimeout(draw, turboMode.value ? 0 : frameScheduler.nextFrameIn);
   }());
   (function frame() {
     if (renderingTerminated) {
       return;
     }
-    if (stage._invalid || stage._mouseMoved) {
+    frameRequested = true;
+    if ((stage._invalid || stage._mouseMoved) && !renderDummyBalls) {
       drawFrame(false, true);
     }
-    frameRequested = true;
     requestAnimationFrame(frame);
   }());
 }
+var FrameScheduler = function () {
+    var STATS_TO_REMEMBER = 50;
+    var MAX_DRAWS_TO_SKIP = 2;
+    var INTERVAL_PADDING_MS = 4;
+    var SPEED_ADJUST_RATE = 0.9;
+    function FrameScheduler() {
+      this._drawStats = [];
+      this._drawStatsSum = 0;
+      this._drawStarted = 0;
+      this._drawsSkipped = 0;
+      this._expectedNextFrameAt = performance.now();
+      this._onTime = true;
+      this._trackDelta = false;
+      this._delta = 0;
+      this._onTimeDelta = 0;
+    }
+    FrameScheduler.prototype = {
+      get shallSkipDraw() {
+        if (this._drawsSkipped >= MAX_DRAWS_TO_SKIP) {
+          return false;
+        }
+        var averageDraw = this._drawStats.length < STATS_TO_REMEMBER ? 0 : this._drawStatsSum / this._drawStats.length;
+        var estimatedDrawEnd = performance.now() + averageDraw;
+        return estimatedDrawEnd + INTERVAL_PADDING_MS > this._expectedNextFrameAt;
+      },
+      get nextFrameIn() {
+        return Math.max(0, this._expectedNextFrameAt - performance.now());
+      },
+      get isOnTime() {
+        return this._onTime;
+      },
+      startFrame: function (frameRate) {
+        var interval = 1000 / frameRate;
+        var adjustedInterval = interval;
+        var delta = this._onTimeDelta + this._delta;
+        if (delta !== 0) {
+          if (delta < 0) {
+            adjustedInterval *= SPEED_ADJUST_RATE;
+          } else if (delta > 0) {
+            adjustedInterval /= SPEED_ADJUST_RATE;
+          }
+          this._onTimeDelta += interval - adjustedInterval;
+        }
+        this._expectedNextFrameAt += adjustedInterval;
+        this._onTime = true;
+      },
+      endFrame: function () {
+        var estimatedNextFrameStart = performance.now() + INTERVAL_PADDING_MS;
+        if (estimatedNextFrameStart > this._expectedNextFrameAt) {
+          if (this._trackDelta) {
+            this._onTimeDelta += this._expectedNextFrameAt - estimatedNextFrameStart;
+            console.log(this._onTimeDelta);
+          }
+          this._expectedNextFrameAt = estimatedNextFrameStart;
+          this._onTime = false;
+        }
+      },
+      startDraw: function () {
+        this._drawsSkipped = 0;
+        this._drawStarted = performance.now();
+      },
+      endDraw: function () {
+        var drawTime = performance.now() - this._drawStarted;
+        this._drawStats.push(drawTime);
+        this._drawStatsSum += drawTime;
+        while (this._drawStats.length > STATS_TO_REMEMBER) {
+          this._drawStatsSum -= this._drawStats.shift();
+        }
+      },
+      skipDraw: function () {
+        this._drawsSkipped++;
+      },
+      setDelta: function (value) {
+        if (!this._trackDelta) {
+          return;
+        }
+        this._delta = value;
+      },
+      startTrackDelta: function () {
+        this._trackDelta = true;
+      },
+      endTrackDelta: function () {
+        if (!this._trackDelta) {
+          return;
+        }
+        this._trackDelta = false;
+        this._delta = 0;
+        this._onTimeDelta = 0;
+      }
+    };
+    return FrameScheduler;
+  }();
 var tagHandler = function (global) {
     function defineShape($bytes, $stream, $, swfVersion, tagCode) {
       $ || ($ = {});
       $.id = readUi16($bytes, $stream);
       var $0 = $.bbox = {};
       bbox($bytes, $stream, $0, swfVersion, tagCode);
       var isMorph = $.isMorph = tagCode === 46 || tagCode === 84;
       if (isMorph) {
@@ -7240,17 +7304,17 @@ var readHeader = function readHeader($by
   $0.yMin = yMin;
   $0.yMax = yMax;
   align($bytes, $stream);
   var frameRateFraction = readUi8($bytes, $stream);
   $.frameRate = readUi8($bytes, $stream) + frameRateFraction / 256;
   $.frameCount = readUi16($bytes, $stream);
   return $;
 };
-function readTags(context, stream, swfVersion, final, onprogress) {
+function readTags(context, stream, swfVersion, final, onprogress, onexception) {
   var tags = context.tags;
   var bytes = stream.bytes;
   var lastSuccessfulPosition;
   var tag = null;
   if (context._readTag) {
     tag = context._readTag;
     delete context._readTag;
   }
@@ -7309,16 +7373,17 @@ function readTags(context, stream, swfVe
       if (onprogress) {
         onprogress(context);
       }
     } else {
       context._readTag = tag;
     }
   } catch (e) {
     if (e !== StreamNoDataError) {
+      onexception && onexception(e);
       throw e;
     }
     stream.pos = lastSuccessfulPosition;
     context._readTag = tag;
   }
 }
 function HeadTailBuffer(defaultSize) {
   this.bufferSize = defaultSize || 16;
@@ -7468,17 +7533,17 @@ BodyParser.prototype = {
     }
     var finalBlock = false;
     if (progressInfo) {
       swf.bytesLoaded = progressInfo.bytesLoaded;
       swf.bytesTotal = progressInfo.bytesTotal;
       finalBlock = progressInfo.bytesLoaded >= progressInfo.bytesTotal;
     }
     var readStartTime = performance.now();
-    readTags(swf, stream, swfVersion, finalBlock, options.onprogress);
+    readTags(swf, stream, swfVersion, finalBlock, options.onprogress, options.onexception);
     swf.parseTime += performance.now() - readStartTime;
     var read = stream.pos;
     buffer.removeHead(read);
     this.totalRead += read;
     if (options.oncomplete && swf.tags[swf.tags.length - 1].finalTag) {
       options.oncomplete(swf);
     }
   }
@@ -7897,16 +7962,23 @@ function createParsingContext(commitData
           height: (bbox.yMax - bbox.yMin) / 20,
           isAvm2: !(!result.fileAttributes.doAbc)
         };
       }
       commitData({
         command: 'complete',
         stats: stats
       });
+    },
+    onexception: function (e) {
+      commitData({
+        type: 'exception',
+        message: e.message,
+        stack: e.stack
+      });
     }
   };
 }
 function parseBytes(bytes, commitData) {
   SWF.parse(bytes, createParsingContext(commitData));
 }
 function ResourceLoader(scope) {
   this.subscription = null;
@@ -9458,16 +9530,21 @@ function interpretActions(actionsData, s
           stack.push(undefined);
         }
       }
       if (!recoveringFromError) {
         if (currentContext.errorsIgnored++ >= MAX_AVM1_ERRORS_LIMIT) {
           throw new AS2CriticalError('long running script -- AVM1 errors limit is reached');
         }
         console.error('AVM1 error: ' + e);
+        avm2.exceptions.push({
+          source: 'avm1',
+          message: e.message,
+          stack: e.stack
+        });
         recoveringFromError = true;
       }
     }
   }
 }
 var ActionTracerFactory = function () {
     var indentation = 0;
     var tracer = {
@@ -24533,21 +24610,30 @@ var ApplicationDomain = function () {
         if (!this.base) {
           Type.initializeTypes(this);
         }
       },
       broadcastMessage: function (type, message, origin) {
         if (false) {
           Timer.start('broadcast: ' + type);
         }
-        this.onMessage.notify1(type, {
-          data: message,
-          origin: origin,
-          source: this
-        });
+        try {
+          this.onMessage.notify1(type, {
+            data: message,
+            origin: origin,
+            source: this
+          });
+        } catch (e) {
+          avm2.exceptions.push({
+            source: type,
+            message: e.message,
+            stack: e.stack
+          });
+          throw e;
+        }
         if (false) {
           Timer.stop();
         }
       },
       traceLoadedClasses: function (lastOnly) {
         var writer = new IndentingWriter();
         lastOnly || writer.enter('Loaded Classes And Interfaces');
         var classes = lastOnly ? [
@@ -33497,49 +33583,66 @@ var Interpreter = new (function () {
               throw e;
             }
           }
       }
     };
     return Interpreter;
   }())();
 var AVM2 = function () {
-    function avm2(sysMode, appMode, findDefiningAbc, loadAVM1) {
+    function findDefiningAbc(mn) {
+      if (!avm2.builtinsLoaded) {
+        return null;
+      }
+      for (var i = 0; i < mn.namespaces.length; i++) {
+        var name = mn.namespaces[i].originalURI + ':' + mn.name;
+        var abcName = playerGlobalNames[name];
+        if (abcName) {
+          break;
+        }
+      }
+      if (abcName) {
+        return grabAbc(abcName);
+      }
+      return null;
+    }
+    function avm2Ctor(sysMode, appMode, loadAVM1) {
       this.systemDomain = new ApplicationDomain(this, null, sysMode, true);
       this.applicationDomain = new ApplicationDomain(this, this.systemDomain, appMode, false);
       this.findDefiningAbc = findDefiningAbc;
       this.loadAVM1 = loadAVM1;
       this.isAVM1Loaded = false;
       this.exception = {
         value: undefined
       };
-    }
-    avm2.currentAbc = function () {
+      this.exceptions = [];
+    }
+    avm2Ctor.currentAbc = function () {
       var caller = arguments.callee;
       var maxDepth = 20;
       var abc = null;
       for (var i = 0; i < maxDepth && caller; i++) {
         var mi = caller.methodInfo;
         if (mi) {
           abc = mi.abc;
           break;
         }
         caller = caller.caller;
       }
       return abc;
     };
-    avm2.currentDomain = function () {
+    avm2Ctor.currentDomain = function () {
       var abc = this.currentAbc();
       return abc.applicationDomain;
     };
-    avm2.prototype = {
+    avm2Ctor.prototype = {
       notifyConstruct: function notifyConstruct(instanceConstructor, args) {
       }
     };
-    return avm2;
+    return avm2Ctor;
   }();
 var playerGlobalNames = {};
 var playerGlobalScripts = {};
 (function () {
   var index = [
       {
         'name': 'Object',
         'offset': 0,
@@ -36931,38 +37034,21 @@ function grabAbc(abcName) {
   var entry = libraryScripts[abcName];
   if (entry) {
     var offset = entry.offset;
     var length = entry.length;
     return new AbcFile(new Uint8Array(libraryAbcs, offset, length), abcName);
   }
   return null;
 }
-function findDefiningAbc(mn) {
-  if (!avm2.builtinsLoaded) {
-    return null;
-  }
-  var name;
-  for (var i = 0; i < mn.namespaces.length; i++) {
-    var name = mn.namespaces[i].originalURI + ':' + mn.name;
-    var abcName = playerGlobalNames[name];
-    if (abcName) {
-      break;
-    }
-  }
-  if (abcName) {
-    return grabAbc(abcName);
-  }
-  return null;
-}
 var avm2;
 var libraryScripts = playerGlobalScripts;
 var libraryNames = playerGlobalNames;
 function createAVM2(builtinPath, libraryPath, avm1Path, sysMode, appMode, next) {
-  avm2 = new AVM2(sysMode, appMode, findDefiningAbc, loadAVM1);
+  avm2 = new AVM2(sysMode, appMode, loadAVM1);
   var builtinAbc, libraryAbc, avm1Abc;
   new BinaryFileReader(libraryPath).readAll(null, function (buffer) {
     libraryAbcs = buffer;
     new BinaryFileReader(builtinPath).readAll(null, function (buffer) {
       builtinAbc = new AbcFile(new Uint8Array(buffer), 'builtin.abc');
       executeAbc();
     });
   });
@@ -37653,17 +37739,17 @@ var DisplayObjectDefinition = function (
                 }
                 node = nextNode;
               } while (node);
             }
           } else {
             m = this._concatenatedTransform;
           }
           if (targetCoordSpace && targetCoordSpace !== this._stage) {
-            m2 = targetCoordMatrix || targetCoordSpace._getConcatenatedTransform();
+            m2 = targetCoordMatrix || targetCoordSpace._getConcatenatedTransform(null, false);
             var a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0;
             if (m2.b || m2.c) {
               var det = 1 / (m2.a * m2.d - m2.b * m2.c);
               a = m2.d * det;
               b = -m2.b * det;
               c = -m2.c * det;
               d = m2.a * det;
               tx = -(a * m2.tx + c * m2.ty);
@@ -37692,24 +37778,24 @@ var DisplayObjectDefinition = function (
               d: m.d * m2.d,
               tx: m.tx * m2.a + m2.tx,
               ty: m.ty * m2.d + m2.ty
             };
           }
           return m;
         },
         _applyCurrentTransform: function (pt) {
-          var m = this._getConcatenatedTransform();
+          var m = this._getConcatenatedTransform(null, false);
           var x = pt.x;
           var y = pt.y;
           pt.x = m.a * x + m.c * y + m.tx | 0;
           pt.y = m.d * y + m.b * x + m.ty | 0;
         },
         _applyConcatenatedInverseTransform: function (pt) {
-          var m = this._getConcatenatedTransform();
+          var m = this._getConcatenatedTransform(null, false);
           var det = 1 / (m.a * m.d - m.b * m.c);
           var x = pt.x - m.tx;
           var y = pt.y - m.ty;
           pt.x = (m.d * x - m.c * y) * det | 0;
           pt.y = (m.a * y - m.b * x) * det | 0;
         },
         _hitTest: function (use_xy, x, y, useShape, hitTestObject) {
           if (use_xy) {
@@ -37740,17 +37826,17 @@ var DisplayObjectDefinition = function (
                     return true;
                   }
                 }
               }
             }
             var children = this._children;
             for (var i = 0, n = children.length; i < n; i++) {
               var child = children[i];
-              if (child._hitTest && child._hitTest(true, x, y, true)) {
+              if (child._hitTest && child._hitTest(true, x, y, true, null)) {
                 return true;
               }
             }
             return false;
           }
           var b1 = this.getBounds(this._stage);
           var b2 = hitTestObject.getBounds(hitTestObject._stage);
           x = Math.max(b1.xMin, b2.xMin);
@@ -38133,16 +38219,19 @@ var DisplayObjectDefinition = function (
               xMax = bbox.xMax;
               yMin = bbox.yMin;
               yMax = bbox.yMax;
             } else {
               var children = this._children;
               var numChildren = children.length;
               for (var i = 0; i < numChildren; i++) {
                 var child = children[i];
+                if (!flash.display.DisplayObject.class.isInstanceOf(child)) {
+                  continue;
+                }
                 var b = child.getBounds(this);
                 var x1 = b.xMin;
                 var y1 = b.yMin;
                 var x2 = b.xMax;
                 var y2 = b.yMax;
                 xMin = Math.min(xMin, x1, x2);
                 xMax = Math.max(xMax, x1, x2);
                 yMin = Math.min(yMin, y1, y2);
@@ -38243,17 +38332,17 @@ var DisplayObjectDefinition = function (
           if (xMax - xMin === 0 || yMax - yMin === 0) {
             return {
               xMin: 0,
               yMin: 0,
               xMax: 0,
               yMax: 0
             };
           }
-          var m = targetCoordSpace && !flash.display.DisplayObject.class.isInstanceOf(targetCoordSpace) ? targetCoordSpace : this._getConcatenatedTransform(targetCoordSpace);
+          var m = targetCoordSpace && !flash.display.DisplayObject.class.isInstanceOf(targetCoordSpace) ? targetCoordSpace : this._getConcatenatedTransform(targetCoordSpace, false);
           var x0 = m.a * xMin + m.c * yMin + m.tx | 0;
           var y0 = m.b * xMin + m.d * yMin + m.ty | 0;
           var x1 = m.a * xMax + m.c * yMin + m.tx | 0;
           var y1 = m.b * xMax + m.d * yMin + m.ty | 0;
           var x2 = m.a * xMax + m.c * yMax + m.tx | 0;
           var y2 = m.b * xMax + m.d * yMax + m.ty | 0;
           var x3 = m.a * xMin + m.c * yMax + m.tx | 0;
           var y3 = m.b * xMin + m.d * yMax + m.ty | 0;
@@ -39836,17 +39925,25 @@ var LoaderDefinition = function () {
           if (WORKERS_ENABLED) {
             worker = new Worker(SHUMWAY_ROOT + LOADER_PATH);
           } else {
             worker = new ResourceLoader(window);
           }
           var loader = this;
           loader._worker = worker;
           worker.onmessage = function (evt) {
-            loader._commitData(evt.data);
+            if (evt.data.type === 'exception') {
+              avm2.exceptions.push({
+                source: 'parser',
+                message: evt.data.message,
+                stack: evt.data.stack
+              });
+            } else {
+              loader._commitData(evt.data);
+            }
           };
           if (flash.net.URLRequest.class.isInstanceOf(request)) {
             var session = FileLoadingService.createSession();
             session.onprogress = function (data, progress) {
               worker.postMessage({
                 data: data,
                 progress: progress
               });
@@ -40438,62 +40535,20 @@ var MovieClipDefinition = function () {
             return this._framesLoaded;
           }
           return frameNum;
         },
         _registerStartSounds: function (frameNum, starts) {
           this._startSoundRegistrations[frameNum] = starts;
         },
         _initSoundStream: function (streamInfo) {
-          var soundStream = this._soundStream = {
-              data: {
-                sampleRate: streamInfo.sampleRate,
-                channels: streamInfo.channels
-              },
-              seekIndex: [],
-              position: 0
-            };
-          var isMP3 = streamInfo.format === 'mp3';
-          if (isMP3 && PLAY_USING_AUDIO_TAG) {
-            var element = document.createElement('audio');
-            if (element.canPlayType('audio/mpeg')) {
-              soundStream.element = element;
-              soundStream.rawFrames = [];
-              return;
-            }
-          }
-          soundStream.data.pcm = new Float32Array(streamInfo.samplesCount * streamInfo.channels);
-          if (isMP3) {
-            soundStream.decoderPosition = 0;
-            soundStream.decoderSession = new MP3DecoderSession();
-            soundStream.decoderSession.onframedata = function (frameData) {
-              var position = soundStream.decoderPosition;
-              soundStream.data.pcm.set(frameData, position);
-              soundStream.decoderPosition = position + frameData.length;
-            }.bind(this);
-            soundStream.decoderSession.onerror = function (error) {
-              console.error('ERROR: MP3DecoderSession: ' + error);
-            };
-          }
+          this._soundStream = new MovieClipSoundStream(streamInfo, this);
         },
         _addSoundStreamBlock: function (frameNum, streamBlock) {
-          var soundStream = this._soundStream;
-          var streamPosition = soundStream.position;
-          soundStream.seekIndex[frameNum] = streamPosition + streamBlock.seek * soundStream.data.channels;
-          soundStream.position = streamPosition + streamBlock.samplesCount * soundStream.data.channels;
-          if (soundStream.rawFrames) {
-            soundStream.rawFrames.push(streamBlock.data);
-            return;
-          }
-          var decoderSession = soundStream.decoderSession;
-          if (decoderSession) {
-            decoderSession.pushAsync(streamBlock.data);
-          } else {
-            soundStream.data.pcm.set(streamBlock.pcm, streamPosition);
-          }
+          this._soundStream.appendBlock(frameNum, streamBlock);
         },
         _startSounds: function (frameNum) {
           var starts = this._startSoundRegistrations[frameNum];
           if (starts) {
             var sounds = this._sounds || (this._sounds = {});
             var loader = this.loaderInfo._loader;
             for (var i = 0; i < starts.length; i++) {
               var start = starts[i];
@@ -40516,52 +40571,18 @@ var MovieClipDefinition = function () {
                 delete sound.channel;
               }
               if (!info.stop) {
                 var loops = info.hasLoops ? info.loopCount : 0;
                 sound.channel = sound.object.play(0, loops);
               }
             }
           }
-          if (this._soundStream && !isNaN(this._soundStream.seekIndex[frameNum])) {
-            var PAUSE_WHEN_OF_SYNC_GREATER = 2;
-            var RESET_WHEN_OF_SYNC_GREATER = 4;
-            var soundStream = this._soundStream;
-            var element = soundStream.element;
-            if (element) {
-              var soundStreamData = soundStream.data;
-              var time = soundStream.seekIndex[frameNum] / soundStreamData.sampleRate / soundStreamData.channels;
-              if (this._complete && !soundStream.channel) {
-                var blob = new Blob(soundStream.rawFrames);
-                element.preload = 'metadata';
-                element.loop = false;
-                element.src = URL.createObjectURL(blob);
-                var symbolClass = flash.media.SoundChannel.class;
-                var channel = symbolClass.createAsSymbol({
-                    element: element
-                  });
-                symbolClass.instanceConstructor.call(channel);
-                soundStream.channel = channel;
-              } else if (!isNaN(element.duration) && element.currentTime > time + PAUSE_WHEN_OF_SYNC_GREATER && element.currentTime < time + RESET_WHEN_OF_SYNC_GREATER) {
-                element.pause();
-              } else if (!isNaN(element.duration) && (element.paused || Math.abs(element.currentTime - time) > RESET_WHEN_OF_SYNC_GREATER)) {
-                if (Math.abs(element.currentTime - time) > PAUSE_WHEN_OF_SYNC_GREATER) {
-                  element.pause();
-                  element.currentTime = time;
-                }
-                element.play();
-              }
-            } else if (!soundStream.sound) {
-              var symbolClass = flash.media.Sound.class;
-              var sound = symbolClass.createAsSymbol(this._soundStream.data);
-              symbolClass.instanceConstructor.call(sound);
-              var channel = sound.play();
-              soundStream.sound = sound;
-              soundStream.channel = channel;
-            }
+          if (this._soundStream) {
+            this._soundStream.playFrame(frameNum);
           }
         },
         _getAS2Object: function () {
           if (!this.$as2Object) {
             if (this._avm1SymbolClass) {
               var nativeObject = this, nativeObjectClass = this._avm1SymbolClass;
               var constructWrapper = function () {
                 this.init(nativeObject);
@@ -40731,16 +40752,187 @@ var MovieClipDefinition = function () {
           addFrameScript: def.addFrameScript,
           prevScene: def.prevScene,
           nextScene: def.nextScene
         }
       }
     };
     return def;
   }.call(this);
+var MovieClipSoundStream = function () {
+    var MP3_MIME_TYPE = 'audio/mpeg';
+    function openMediaSource(soundStream, mediaSource) {
+      var sourceBuffer;
+      try {
+        sourceBuffer = mediaSource.addSourceBuffer(MP3_MIME_TYPE);
+        soundStream.mediaSource = mediaSource;
+        soundStream.sourceBuffer = sourceBuffer;
+        soundStream.rawFrames.forEach(function (data) {
+          sourceBuffer.appendBuffer(data);
+        });
+        delete soundStream.rawFrames;
+      } catch (e) {
+        console.error('MediaSource mp3 playback is not supported: ' + e);
+      }
+    }
+    function syncTime(element, movieClip) {
+      var initialized = false;
+      var startMediaTime, startRealTime;
+      element.addEventListener('timeupdate', function (e) {
+        if (!initialized) {
+          startMediaTime = element.currentTime;
+          startRealTime = performance.now();
+          initialized = true;
+          movieClip._stage._frameScheduler.startTrackDelta();
+          return;
+        }
+        var mediaDelta = element.currentTime - startMediaTime;
+        var realDelta = performance.now() - startRealTime;
+        movieClip._stage._frameScheduler.setDelta(realDelta - mediaDelta * 1000);
+      });
+      element.addEventListener('pause', function (e) {
+        movieClip._stage._frameScheduler.endTrackDelta();
+        initialized = false;
+      });
+      element.addEventListener('seeking', function (e) {
+        movieClip._stage._frameScheduler.endTrackDelta();
+        initialized = false;
+      });
+    }
+    function MovieClipSoundStream(streamInfo, movieClip) {
+      this.movieClip = movieClip;
+      this.data = {
+        sampleRate: streamInfo.sampleRate,
+        channels: streamInfo.channels
+      };
+      this.seekIndex = [];
+      this.position = 0;
+      var isMP3 = streamInfo.format === 'mp3';
+      if (isMP3 && PLAY_USING_AUDIO_TAG) {
+        var element = document.createElement('audio');
+        element.preload = 'metadata';
+        element.loop = false;
+        syncTime(element, movieClip);
+        if (element.canPlayType(MP3_MIME_TYPE)) {
+          this.element = element;
+          if (typeof MediaSource !== 'undefined') {
+            var mediaSource = new MediaSource();
+            mediaSource.addEventListener('sourceopen', openMediaSource.bind(null, this, mediaSource));
+            element.src = URL.createObjectURL(mediaSource);
+          } else {
+            console.warn('MediaSource is not supported');
+          }
+          this.rawFrames = [];
+          return;
+        }
+      }
+      var totalSamples = streamInfo.samplesCount * streamInfo.channels;
+      this.data.pcm = new Float32Array(totalSamples);
+      if (isMP3) {
+        var soundStream = this;
+        soundStream.decoderPosition = 0;
+        soundStream.decoderSession = new MP3DecoderSession();
+        soundStream.decoderSession.onframedata = function (frameData) {
+          var position = soundStream.decoderPosition;
+          soundStream.data.pcm.set(frameData, position);
+          soundStream.decoderPosition = position + frameData.length;
+        }.bind(this);
+        soundStream.decoderSession.onerror = function (error) {
+          console.error('ERROR: MP3DecoderSession: ' + error);
+        };
+      }
+    }
+    MovieClipSoundStream.prototype = {
+      appendBlock: function (frameNum, streamBlock) {
+        var streamPosition = this.position;
+        this.seekIndex[frameNum] = streamPosition + streamBlock.seek * this.data.channels;
+        this.position = streamPosition + streamBlock.samplesCount * this.data.channels;
+        if (this.sourceBuffer) {
+          this.sourceBuffer.appendBuffer(streamBlock.data);
+          return;
+        }
+        if (this.rawFrames) {
+          this.rawFrames.push(streamBlock.data);
+          return;
+        }
+        var decoderSession = this.decoderSession;
+        if (decoderSession) {
+          decoderSession.pushAsync(streamBlock.data);
+        } else {
+          this.data.pcm.set(streamBlock.pcm, streamPosition);
+        }
+      },
+      playFrame: function (frameNum) {
+        if (isNaN(this.seekIndex[frameNum])) {
+          return;
+        }
+        var PAUSE_WHEN_OF_SYNC_GREATER = 1;
+        var PLAYBACK_ADJUSTMENT = 0.25;
+        var element = this.element;
+        if (element) {
+          var soundStreamData = this.data;
+          var time = this.seekIndex[frameNum] / soundStreamData.sampleRate / soundStreamData.channels;
+          if (!this.channel && (this.movieClip._complete || this.sourceBuffer)) {
+            if (!this.sourceBuffer) {
+              var blob = new Blob(this.rawFrames);
+              element.src = URL.createObjectURL(blob);
+            }
+            var symbolClass = flash.media.SoundChannel.class;
+            var channel = symbolClass.createAsSymbol({
+                element: element
+              });
+            symbolClass.instanceConstructor.call(channel);
+            this.channel = channel;
+            this.expectedFrame = 0;
+            this.waitFor = 0;
+          } else if (this.sourceBuffer || !isNaN(element.duration)) {
+            if (this.mediaSource && this.movieClip._complete) {
+              this.mediaSource.endOfStream();
+              this.mediaSource = null;
+            }
+            var elementTime = element.currentTime;
+            if (this.expectedFrame !== frameNum) {
+              if (element.paused) {
+                element.play();
+                element.addEventListener('playing', function setTime(e) {
+                  element.removeEventListener('playing', setTime);
+                  element.currentTime = time;
+                });
+              } else {
+                element.currentTime = time;
+              }
+            } else if (this.waitFor > 0) {
+              if (this.waitFor <= time) {
+                if (element.paused) {
+                  element.play();
+                }
+                this.waitFor = 0;
+              }
+            } else if (elementTime - time > PAUSE_WHEN_OF_SYNC_GREATER) {
+              console.warn('Sound is faster than frames by ' + (elementTime - time));
+              this.waitFor = elementTime - PLAYBACK_ADJUSTMENT;
+              element.pause();
+            } else if (time - elementTime > PAUSE_WHEN_OF_SYNC_GREATER) {
+              console.warn('Sound is slower than frames by ' + (time - elementTime));
+              element.currentTime = time + PLAYBACK_ADJUSTMENT;
+            }
+            this.expectedFrame = frameNum + 1;
+          }
+        } else if (!this.sound) {
+          var symbolClass = flash.media.Sound.class;
+          var sound = symbolClass.createAsSymbol(this.data);
+          symbolClass.instanceConstructor.call(sound);
+          var channel = sound.play();
+          this.sound = sound;
+          this.channel = channel;
+        }
+      }
+    };
+    return MovieClipSoundStream;
+  }();
 var NativeMenuDefinition = function () {
     return {
       __class__: 'flash.display.NativeMenu',
       initialize: function () {
       },
       __glue__: {
         native: {
           static: {},
@@ -41517,17 +41709,17 @@ var StageDefinition = function () {
         this._mouseMoved = false;
         this._mouseTarget = this;
         this._mouseEvents = [];
         this._cursor = 'auto';
         this._stageVideos = [];
         this._concatenatedTransform.invalid = false;
       },
       _setup: function setup(ctx, options) {
-        this._qtree = new QuadTree(0, 0, this._stageWidth, this._stageHeight, 0);
+        this._qtree = new QuadTree(0, 0, this._stageWidth, this._stageHeight, null);
         this._invalid = true;
       },
       _addToStage: function addToStage(displayObject) {
         displayObject._stage = this;
         var parent = displayObject._parent;
         displayObject._level = parent._level + 1;
         displayObject._invalid = true;
         var children = displayObject._children;
@@ -42135,54 +42327,63 @@ var EventDispatcherDefinition = function
         processListeners(listeners, event, eventClass, bubbles, target);
       }
       return !event._isDefaultPrevented;
     }
     function processListeners(queue, event, eventClass, bubbles, target, currentTarget, eventPhase) {
       if (queue) {
         queue = queue.slice();
         var needsInit = true;
-        for (var i = 0; i < queue.length; i++) {
-          var item = queue[i];
-          var methodInfo = item.handleEvent.methodInfo;
-          if (methodInfo) {
-            if (methodInfo.parameters.length) {
-              if (!methodInfo.parameters[0].isUsed) {
-                item.handleEvent();
-                continue;
-              }
-            }
-          }
-          if (needsInit) {
-            if (typeof event === 'string') {
-              if (eventClass) {
-                event = new eventClass(event);
-              } else {
-                if (event in mouseEvents) {
-                  event = new flash.events.MouseEvent(event, mouseEvents[event]);
-                  if (target._stage) {
-                    event._localX = target.mouseX;
-                    event._localY = target.mouseY;
+        try {
+          for (var i = 0; i < queue.length; i++) {
+            var item = queue[i];
+            var methodInfo = item.handleEvent.methodInfo;
+            if (methodInfo) {
+              if (methodInfo.parameters.length) {
+                if (!methodInfo.parameters[0].isUsed) {
+                  item.handleEvent();
+                  continue;
+                }
+              }
+            }
+            if (needsInit) {
+              if (typeof event === 'string') {
+                if (eventClass) {
+                  event = new eventClass(event);
+                } else {
+                  if (event in mouseEvents) {
+                    event = new flash.events.MouseEvent(event, mouseEvents[event]);
+                    if (target._stage) {
+                      event._localX = target.mouseX;
+                      event._localY = target.mouseY;
+                    }
+                  } else {
+                    event = new flash.events.Event(event);
                   }
-                } else {
-                  event = new flash.events.Event(event);
-                }
-              }
-            } else if (event._target) {
-              event = event.clone();
-            }
-            event._target = target;
-            event._currentTarget = currentTarget || target;
-            event._eventPhase = eventPhase || 2;
-            needsInit = false;
-          }
-          item.handleEvent(event);
-          if (event._stopImmediatePropagation) {
-            break;
-          }
+                }
+              } else if (event._target) {
+                event = event.clone();
+              }
+              event._target = target;
+              event._currentTarget = currentTarget || target;
+              event._eventPhase = eventPhase || 2;
+              needsInit = false;
+            }
+            item.handleEvent(event);
+            if (event._stopImmediatePropagation) {
+              break;
+            }
+          }
+        } catch (e) {
+          avm2.exceptions.push({
+            source: 'avm2',
+            message: e.message,
+            stack: e.stack
+          });
+          throw e;
         }
       }
       return !event._stopPropagation;
     }
     return {
       __class__: 'flash.events.EventDispatcher',
       initialize: function () {
         this._target = this;
@@ -42342,25 +42543,25 @@ var MouseEventDefinition = function () {
       },
       __glue__: {
         native: {
           instance: {
             updateAfterEvent: function updateAfterEvent() {
             },
             getStageX: function getStageX() {
               if (this._target) {
-                var m = this._target._getConcatenatedTransform();
+                var m = this._target._getConcatenatedTransform(null, false);
                 var x = m.a * this._localX + m.c * this._localY + m.tx;
                 return x / 20;
               }
               return this._localX / 20;
             },
             getStageY: function getStageY() {
               if (this._target) {
-                var m = this._target._getConcatenatedTransform();
+                var m = this._target._getConcatenatedTransform(null, false);
                 var y = m.d * this._localY + m.b * this._localX + m.ty;
                 return y / 20;
               }
               return this._localY / 20;
             },
             localX: {
               get: function localX() {
                 return this._localX / 20;
@@ -43276,17 +43477,17 @@ var TransformDefinition = function () {
           var cxform = this.colorTransform;
           cxform.concat(this._target.parent.transform.concatenatedColorTransform);
           return cxform;
         },
         get concatenatedMatrix() {
           if (this._target._current3DTransform) {
             return null;
           }
-          var m = this._target._getConcatenatedTransform();
+          var m = this._target._getConcatenatedTransform(null, false);
           return new flash.geom.Matrix(m.a, m.b, m.c, m.d, m.tx / 20, m.ty / 20);
         },
         get matrix() {
           if (this._target._current3DTransform) {
             return null;
           }
           var m = this._target._currentTransform;
           return new flash.geom.Matrix(m.a, m.b, m.c, m.d, m.tx / 20, m.ty / 20);
@@ -49317,517 +49518,95 @@ var StyleSheetDefinition = function () {
               }
             }
           }
         }
       }
     };
   }.call(this);
 var TextFieldDefinition = function () {
-    var htmlParser = document.createElement('p');
-    var measureCtx = document.createElement('canvas').getContext('2d');
-    function TextLine(y) {
-      this.x = 0;
-      this.width = 0;
-      this.y = y;
-      this.height = 0;
-      this.leading = 0;
-      this.runs = [];
-      this.largestFormat = null;
-    }
-    function parseHtml(val, initialFormat, multiline) {
-      htmlParser.innerHTML = val;
-      var rootElement = htmlParser.childNodes.length !== 1 ? htmlParser : htmlParser.childNodes[0];
-      var content = {
-          text: '',
-          htmlText: val,
-          tree: createTrunk(initialFormat)
-        };
-      if (rootElement.nodeType === 3) {
-        convertNode(rootElement, content.tree.children[0].children, content, multiline);
-        return content;
-      }
-      var initialNodeList = [
-          rootElement
-        ];
-      var attributes;
-      var format;
-      var key;
-      if (initialNodeList.length == 1 && rootElement.localName.toUpperCase() == 'P') {
-        attributes = extractAttributes(rootElement);
-        format = content.tree.format;
-        for (key in attributes) {
-          format[key] = attributes[key];
-        }
-        initialNodeList = rootElement.childNodes;
-        rootElement = rootElement.childNodes[0];
-      }
-      if (initialNodeList.length == 1 && rootElement.localName.toUpperCase() == 'FONT') {
-        attributes = extractAttributes(rootElement);
-        format = content.tree.children[0].format;
-        for (key in attributes) {
-          format[key] = attributes[key];
-        }
-        initialNodeList = rootElement.childNodes;
-      }
-      convertNodeList(initialNodeList, content.tree.children[0].children, content, multiline);
-      return content;
-    }
-    function createTrunk(initialFormat) {
-      var trunk = {
-          type: 'SPAN',
-          format: {
-            ALIGN: initialFormat.align
-          },
-          children: []
-        };
-      var fontAttributes = {
-          FACE: initialFormat.face,
-          LETTERSPACING: initialFormat.letterSpacing,
-          KERNING: initialFormat.kerning,
-          LEADING: initialFormat.leading,
-          COLOR: initialFormat.color
-        };
-      trunk.children[0] = {
-        type: 'FONT',
-        format: fontAttributes,
-        children: []
-      };
-      return trunk;
-    }
-    var knownNodeTypes = {
-        'BR': true,
-        'LI': true,
-        'P': true,
-        'B': true,
-        'I': true,
-        'FONT': true,
-        'TEXTFORMAT': true,
-        'U': true,
-        'A': true,
-        'IMG': true,
-        'SPAN': true
-      };
-    function convertNode(input, destinationList, content, multiline) {
-      if (!(input.nodeType === 1 || input.nodeType === 3) || input.prefix) {
-        return;
-      }
-      var node;
-      if (input.nodeType === 3) {
-        var text = input.textContent;
-        node = {
-          type: 'text',
-          text: text,
-          format: null,
-          children: null
-        };
-        content.text += text;
-        destinationList.push(node);
-        return;
-      }
-      var nodeType = input.localName.toUpperCase();
-      if (!knownNodeTypes[nodeType] || multiline === false && (nodeType === 'P' || nodeType === 'BR')) {
-        if (nodeType === 'SBR') {
-          destinationList.push({
-            type: 'BR',
-            text: null,
-            format: null,
-            children: null
-          });
-        }
-        convertNodeList(input.childNodes, destinationList, content, multiline);
-        return;
-      }
-      node = {
-        type: nodeType,
-        text: null,
-        format: extractAttributes(input),
-        children: []
-      };
-      convertNodeList(input.childNodes, node.children, content, multiline);
-      destinationList.push(node);
-    }
-    function convertNodeList(from, to, content, multiline) {
-      var childCount = from.length;
-      for (var i = 0; i < childCount; i++) {
-        convertNode(from[i], to, content, multiline);
-      }
-    }
-    function extractAttributes(node) {
-      var attributesList = node.attributes;
-      var attributesMap = {};
-      for (var i = 0; i < attributesList.length; i++) {
-        var attr = attributesList[i];
-        if (attr.prefix) {
-          continue;
-        }
-        attributesMap[attr.localName.toUpperCase()] = attr.value;
-      }
-      return attributesMap;
-    }
-    function collectRuns(node, state) {
-      var formatNode = false;
-      var blockNode = false;
-      switch (node.type) {
-      case 'plain-text':
-        var lines = node.lines;
-        for (var i = 0; i < lines.length; i++) {
-          addRunsForText(state, lines[i]);
-          if (i < lines.length - 1) {
-            finishLine(state, true);
-          }
-        }
-        return;
-      case 'text':
-        addRunsForText(state, node.text);
-        return;
-      case 'BR':
-        finishLine(state, true);
-        return;
-      case 'LI':
-      case 'P':
-        finishLine(state, false);
-        pushFormat(state, node);
-        blockNode = true;
-        break;
-      case 'B':
-      case 'I':
-      case 'FONT':
-      case 'TEXTFORMAT':
-        pushFormat(state, node);
-        formatNode = true;
-        break;
-      case 'U':
-      case 'A':
-      case 'IMG':
-      case 'SPAN':
-      default:
-      }
-      for (var i = 0; i < node.children.length; i++) {
-        var child = node.children[i];
-        collectRuns(child, state);
-      }
-      if (formatNode) {
-        popFormat(state);
-      }
-      if (blockNode) {
-        finishLine(state, true);
-      }
-    }
-    var WRAP_OPPORTUNITIES = {
-        ' ': true,
-        '.': true,
-        '-': true,
-        '\t': true
-      };
-    function addRunsForText(state, text) {
-      if (!text) {
-        return;
-      }
-      if (!state.wordWrap) {
-        addTextRun(state, text, state.ctx.measureText(text).width);
-        return;
-      }
-      while (text.length) {
-        var width = state.ctx.measureText(text).width;
-        var availableWidth = state.w - state.line.width;
-        if (availableWidth <= 0) {
-          finishLine(state, false);
-          availableWidth = state.w - state.line.width;
-        }
-        if (width <= availableWidth) {
-          addTextRun(state, text, width);
-          break;
-        } else {
-          var offset = text.length / width * availableWidth | 0;
-          while (state.ctx.measureText(text.substr(0, offset)).width < availableWidth && offset < text.length) {
-            offset++;
-          }
-          var wrapOffset = offset;
-          while (wrapOffset > -1) {
-            if (WRAP_OPPORTUNITIES[text[wrapOffset]]) {
-              wrapOffset++;
-              break;
-            }
-            wrapOffset--;
-          }
-          if (wrapOffset === -1) {
-            if (state.line.width > 0) {
-              finishLine(state, false);
-              continue;
-            }
-            while (state.ctx.measureText(text.substr(0, offset)).width > availableWidth) {
-              offset--;
-            }
-            if (offset === 0) {
-              offset = 1;
-            }
-            wrapOffset = offset;
-          }
-          var runText = text.substr(0, wrapOffset);
-          width = state.ctx.measureText(runText).width;
-          addTextRun(state, runText, width);
-          if (state.wordWrap) {
-            finishLine(state, false);
-          }
-          text = text.substr(wrapOffset);
-        }
-      }
-    }
-    function addTextRun(state, text, width) {
-      if (text.length === 0) {
-        return;
-      }
-      var line = state.line;
-      var format = state.currentFormat;
-      var size = format.size;
-      var run = {
-          type: 't',
-          text: text,
-          x: line.width
-        };
-      state.runs.push(run);
-      state.line.runs.push(run);
-      line.width += width | 0;
-      if (line.leading === 0 && format.leading > line.leading) {
-        line.leading = format.leading;
-      }
-      if (!line.largestFormat || size > line.largestFormat.size) {
-        line.largestFormat = format;
-      }
-    }
-    function finishLine(state, forceNewline) {
-      var line = state.line;
-      if (line.runs.length === 0) {
-        if (forceNewline) {
-          var format = state.currentFormat;
-          state.line.y += format.font._metrics.height * format.size + format.leading | 0;
-        }
-        return;
-      }
-      var runs = line.runs;
-      var format = line.largestFormat;
-      var baselinePos = line.y + format.font._metrics.ascent * format.size;
-      for (var i = runs.length; i--;) {
-        runs[i].y = baselinePos;
-      }
-      var align = (state.currentFormat.align || '').toLowerCase();
-      if (state.combinedAlign === null) {
-        state.combinedAlign = align;
-      } else if (state.combinedAlign !== align) {
-        state.combinedAlign = 'mixed';
-      }
-      if (align === 'center' || align === 'right') {
-        var offset = Math.max(state.w - line.width, 0);
-        if (align === 'center') {
-          offset >>= 1;
-        }
-        for (i = runs.length; i--;) {
-          runs[i].x += offset;
-        }
-      }
-      line.height = format.font._metrics.height * format.size + line.leading | 0;
-      state.maxLineWidth = Math.max(state.maxLineWidth, line.width);
-      state.lines.push(line);
-      state.line = new TextLine(line.y + line.height);
-    }
-    function pushFormat(state, node) {
-      var attributes = node.format;
-      var format = Object.create(state.formats[state.formats.length - 1]);
-      var fontChanged = false;
-      switch (node.type) {
-      case 'P':
-        if (attributes.ALIGN === format.align) {
-          return;
-        }
-        format.align = attributes.ALIGN;
-        break;
-      case 'B':
-        format.bold = true;
-        fontChanged = true;
-        break;
-      case 'I':
-        format.italic = true;
-        fontChanged = true;
-        break;
-      case 'FONT':
-        if (attributes.COLOR !== undefined) {
-          format.color = attributes.COLOR;
-        }
-        if (attributes.FACE !== undefined) {
-          format.face = attributes.FACE;
-          fontChanged = true;
-        }
-        if (attributes.SIZE !== undefined) {
-          format.size = parseFloat(attributes.SIZE);
-        }
-        if (attributes.LETTERSPACING !== undefined) {
-          format.letterspacing = parseFloat(attributes.LETTERSPACING);
-        }
-        if (attributes.KERNING !== undefined) {
-          format.kerning = attributes.KERNING && true;
-        }
-      case 'TEXTFORMAT':
-        if (attributes.LEADING !== undefined) {
-          format.leading = parseFloat(attributes.LEADING);
-        }
-        if (attributes.INDENT !== undefined) {
-          state.line.x = attributes.INDENT;
-          state.line.width += attributes.INDENT | 0;
-        }
-        break;
-      default:
-        warning('Unknown format node encountered: ' + node.type);
-        return;
-      }
-      if (state.textColor !== null) {
-        format.color = rgbIntAlphaToStr(state.textColor, 1);
-      }
-      if (fontChanged) {
-        resolveFont(format, state.embedFonts);
-      }
-      format.str = makeFormatString(format);
-      state.formats.push(format);
-      state.runs.push({
-        type: 'f',
-        format: format
-      });
-      state.currentFormat = format;
-      state.ctx.font = format.str;
-    }
-    function popFormat(state) {
-      state.formats.pop();
-      var format = state.currentFormat = state.formats[state.formats.length - 1];
-      state.runs.push({
-        type: 'f',
-        format: format
-      });
-      state.ctx.font = state.str;
-    }
-    function makeFormatString(format) {
-      var boldItalic = '';
-      if (format.italic) {
-        boldItalic += 'italic';
-      }
-      if (format.bold) {
-        boldItalic += ' bold';
-      }
-      return boldItalic + ' ' + format.size + 'px ' + (format.font._uniqueName || format.font._fontName);
-    }
-    function resolveFont(format, embedded) {
-      var face = format.face.toLowerCase();
-      if (face === '_sans') {
-        face = 'sans-serif';
-      } else if (face === '_serif') {
-        face = 'serif';
-      } else if (face === '_typewriter') {
-        face = 'monospace';
-      }
-      var style;
-      if (format.bold) {
-        if (format.italic) {
-          style = 'boldItalic';
-        } else {
-          style = 'bold';
-        }
-      } else if (format.italic) {
-        style = 'italic';
-      } else {
-        style = 'regular';
-      }
-      var font = FontDefinition.getFont(face, style, embedded);
-      format.font = font;
-    }
     var def = {
         __class__: 'flash.text.TextField',
         initialize: function () {
-          var initialFormat = this._defaultTextFormat = {
+          this._bbox = {
+            xMin: 0,
+            yMin: 0,
+            xMax: 2000,
+            yMax: 2000
+          };
+          var initialFormat = {
               align: 'LEFT',
               face: 'serif',
               size: 12,
               letterspacing: 0,
               kerning: 0,
               color: 0,
               leading: 0
             };
+          this._content = new TextFieldContent(initialFormat);
           this._type = 'dynamic';
+          this._embedFonts = false;
           this._selectable = true;
-          this._textWidth = 0;
-          this._textHeight = 0;
+          this._autoSize = 'none';
           this._scrollV = 1;
           this._maxScrollV = 1;
           this._bottomScrollV = 1;
-          this._lines = [];
-          this._embedFonts = false;
-          this._autoSize = 'none';
-          this._wordWrap = false;
-          this._multiline = false;
-          this._condenseWhite = false;
+          this._drawingOffsetH = 0;
           this._background = false;
           this._border = false;
           this._backgroundColor = 16777215;
           this._backgroundColorStr = '#ffffff';
           this._borderColor = 0;
           this._borderColorStr = '#000000';
-          this._textColor = null;
-          this._drawingOffsetH = 0;
-          this._bbox = {
-            xMin: 0,
-            yMin: 0,
-            xMax: 2000,
-            yMax: 2000
-          };
           var s = this.symbol;
           if (!s) {
             this._currentTransform.tx -= 40;
             this._currentTransform.ty -= 40;
-            resolveFont(initialFormat, false);
+            this._content.resolveFont(initialFormat, false);
             this.text = '';
             return;
           }
           var tag = s.tag;
           var bbox = tag.bbox;
           this._currentTransform.tx += bbox.xMin;
           this._currentTransform.ty += bbox.yMin;
           this._bbox.xMax = bbox.xMax - bbox.xMin;
           this._bbox.yMax = bbox.yMax - bbox.yMin;
           if (tag.hasLayout) {
             initialFormat.size = tag.fontHeight / 20;
-            if (typeof initialFormat.leading === 'number') {
-              initialFormat.leading = tag.leading / 20;
-            }
+            initialFormat.leading = (tag.leading | 0) / 20;
           }
           if (tag.hasColor) {
             initialFormat.color = rgbaObjToStr(tag.color);
           }
           if (tag.hasFont) {
             var font = FontDefinition.getFontByUniqueName(tag.font);
             initialFormat.font = font;
             initialFormat.face = font._fontName;
             initialFormat.bold = font.symbol.bold;
             initialFormat.italic = font.symbol.italic;
-            initialFormat.str = makeFormatString(initialFormat);
-          }
+            initialFormat.str = this._content.makeFormatString(initialFormat);
+          }
+          this._content.multiline = !(!tag.multiline);
+          this._content.wordWrap = !(!tag.wordWrap);
           this._embedFonts = !(!tag.useOutlines);
+          this._selectable = !tag.noSelect;
+          this._border = !(!tag.border);
           switch (tag.align) {
           case 1:
             initialFormat.align = 'right';
             break;
           case 2:
             initialFormat.align = 'center';
             break;
           case 3:
             initialFormat.align = 'justified';
             break;
           default:
           }
-          this._selectable = !tag.noSelect;
-          this._multiline = !(!tag.multiline);
-          this._wordWrap = !(!tag.wordWrap);
-          this._border = !(!tag.border);
           if (tag.initialText) {
             if (tag.html) {
               this.htmlText = tag.initialText;
             } else {
               this.text = tag.initialText;
             }
           } else {
             this.text = '';
@@ -49861,25 +49640,25 @@ var TextFieldDefinition = function () {
           }
           if (this._border) {
             colorTransform.setStrokeStyle(ctx, this._borderColorStr);
             ctx.lineCap = 'square';
             ctx.lineWidth = 1;
             ctx.strokeRect(0.5, 0.5, width | 0, height | 0);
           }
           ctx.closePath();
-          if (this._lines.length === 0) {
+          if (this._content.lines.length === 0) {
             ctx.restore();
             return;
           }
           ctx.translate(2, 2);
           ctx.save();
           colorTransform.setAlpha(ctx);
-          var runs = this._content.textruns;
-          var offsetY = this._lines[this._scrollV - 1].y;
+          var runs = this._content._textRuns;
+          var offsetY = this._content.lines[this._scrollV - 1].y;
           for (var i = 0; i < runs.length; i++) {
             var run = runs[i];
             if (run.type === 'f') {
               ctx.restore();
               ctx.font = run.format.str;
               colorTransform.setFillStyle(ctx, run.format.color);
               ctx.save();
               colorTransform.setAlpha(ctx);
@@ -49898,68 +49677,38 @@ var TextFieldDefinition = function () {
           this._invalidateBounds();
           this._dimensionsValid = false;
         },
         ensureDimensions: function () {
           if (this._dimensionsValid) {
             return;
           }
           var bounds = this._bbox;
-          var initialFormat = this._defaultTextFormat;
-          resolveFont(initialFormat, this._embedFonts);
-          var firstRun = {
-              type: 'f',
-              format: initialFormat
-            };
-          var width = Math.max(bounds.xMax / 20 - 4, 1);
-          var height = Math.max(bounds.yMax / 20 - 4, 1);
-          var state = {
-              ctx: measureCtx,
-              w: width,
-              h: height,
-              maxLineWidth: 0,
-              formats: [
-                initialFormat
-              ],
-              currentFormat: initialFormat,
-              line: new TextLine(0),
-              lines: [],
-              runs: [
-                firstRun
-              ],
-              wordWrap: this._wordWrap,
-              combinedAlign: null,
-              textColor: this._textColor,
-              embedFonts: this._embedFonts
-            };
-          collectRuns(this._content.tree, state);
-          finishLine(state, false);
-          this._textWidth = state.maxLineWidth | 0;
-          this._textHeight = state.line.y | 0;
-          this._lines = state.lines;
-          this._content.textruns = state.runs;
+          var combinedAlign = this._content.calculateMetrics(bounds, this._embedFonts);
           this._scrollV = 1;
           this._maxScrollV = 1;
           this._bottomScrollV = 1;
           var autoSize = this._autoSize;
           if (autoSize === 'none') {
             var maxVisibleY = (bounds.yMax - 80) / 20;
-            if (this._textHeight > maxVisibleY) {
-              for (var i = 0; i < state.lines.length; i++) {
-                var line = state.lines[i];
+            if (this._content.textHeight > maxVisibleY) {
+              var lines = this._content.lines;
+              for (var i = 0; i < lines.length; i++) {
+                var line = lines[i];
                 if (line.y + line.height > maxVisibleY) {
                   this._maxScrollV = i + 1;
                   this._bottomScrollV = i === 0 ? 1 : i;
                   break;
                 }
               }
             }
           } else {
-            var targetWidth = this._textWidth;
-            var align = state.combinedAlign;
+            var width = Math.max(bounds.xMax / 20 - 4, 1);
+            var targetWidth = this._content.textWidth;
+            var align = combinedAlign;
             var diffX = 0;
             if (align !== 'mixed') {
               switch (autoSize) {
               case 'left':
                 break;
               case 'center':
                 diffX = width - targetWidth >> 1;
                 break;
@@ -49985,82 +49734,51 @@ var TextFieldDefinition = function () {
                   offset >>= 1;
                 }
                 this._drawingOffsetH = offset;
               }
               this._invalidateTransform();
               this._currentTransform.tx += diffX * 20 | 0;
               bounds.xMax = (targetWidth * 20 | 0) + 80;
             }
-            bounds.yMax = (this._textHeight * 20 | 0) + 80;
+            bounds.yMax = (this._content.textHeight * 20 | 0) + 80;
+            console.log(bounds.yMax);
             this._invalidateBounds();
           }
           this._dimensionsValid = true;
         },
         get text() {
           return this._content.text;
         },
         set text(val) {
-          val = String(val);
-          if (this._content && this._content.text === val) {
-            return;
-          }
-          var lines = [];
-          var lineOffset = 0;
-          for (var index = 0; index < val.length;) {
-            var char = val[index];
-            if (char === '\r' || char === '\n') {
-              lines.push(val.substring(lineOffset, index));
-              lineOffset = index;
-              if (char === '\r' && val[index + 1] === '\n') {
-                index++;
-              }
-            }
-            index++;
-          }
-          lines.push(val.substring(lineOffset, index));
-          this._content = {
-            tree: createTrunk(this._defaultTextFormat),
-            text: val,
-            htmlText: val
-          };
-          this._content.tree.children[0].children[0] = {
-            type: 'plain-text',
-            lines: lines
-          };
+          this._content.text = val;
           this.invalidateDimensions();
         },
         get htmlText() {
           return this._content.htmlText;
         },
         set htmlText(val) {
-          if (this._htmlText === val) {
-            return;
-          }
-          this._defaultTextFormat.bold = false;
-          this._defaultTextFormat.italic = false;
-          this._content = parseHtml(val, this._defaultTextFormat, this._multiline);
+          this._content.htmlText = val;
           this.invalidateDimensions();
         },
         get defaultTextFormat() {
-          return new flash.text.TextFormat().fromObject(this._defaultTextFormat);
+          var format = this._content.defaultTextFormat;
+          return new flash.text.TextFormat().fromObject(format);
         },
         set defaultTextFormat(val) {
-          this._defaultTextFormat = val.toObject();
+          this._content.defaultTextFormat = val.toObject();
           this.invalidateDimensions();
         },
         getTextFormat: function (beginIndex, endIndex) {
           return this.defaultTextFormat;
         },
         setTextFormat: function (format, beginIndex, endIndex) {
           this.defaultTextFormat = format;
-          if (this._content && this._content.text === this._content.htmlText) {
-            var text = this.text;
-            this._content = null;
-            this.text = text;
+          if (this.text === this.htmlText) {
+            this.text = this.text;
           }
           this.invalidateDimensions();
         },
         get x() {
           this.ensureDimensions();
           return this._currentTransform.tx;
         },
         set x(val) {
@@ -50098,20 +49816,20 @@ var TextFieldDefinition = function () {
           this.ensureDimensions();
           return this._bbox;
         },
         _getRegion: function getRegion(targetCoordSpace) {
           return this._getTransformedRect(this._getContentBounds(), targetCoordSpace);
         },
         getLineMetrics: function (lineIndex) {
           this.ensureDimensions();
-          if (lineIndex < 0 || lineIndex >= this._lines.length) {
+          if (lineIndex < 0 || lineIndex >= this._content.lines.length) {
             throwError('RangeError', Errors.ParamRangeError);
           }
-          var line = this._lines[lineIndex];
+          var line = this._content.lines[lineIndex];
           var format = line.largestFormat;
           var metrics = format.font._metrics;
           var size = format.size;
           var ascent = metrics.ascent * size + 0.49999 | 0;
           var descent = metrics.descent * size + 0.49999 | 0;
           var leading = metrics.leading * size + 0.49999 + line.leading | 0;
           return new flash.text.TextLineMetrics(line.x + 2, line.width, line.height, ascent, descent, leading);
         },
@@ -50141,80 +49859,80 @@ var TextFieldDefinition = function () {
                 return;
               }
               this._autoSize = value;
               this.invalidateDimensions();
             }
           },
           multiline: {
             get: function multiline() {
-              return this._multiline;
+              return this._content.multiline;
             },
             set: function multiline(value) {
-              if (this._multiline === value) {
+              if (this._content.multiline === value) {
                 return;
               }
-              this._multiline = value;
+              this._content.multiline = value;
               this.invalidateDimensions();
             }
           },
           textColor: {
             get: function textColor() {
-              return this._textColor;
+              return this._content.textColor;
             },
             set: function textColor(value) {
-              if (this._textColor === value) {
+              if (this._content.textColor === value) {
                 return;
               }
-              this._textColor = value;
+              this._content.textColor = value;
               this._invalidate();
             }
           },
           selectable: {
             get: function selectable() {
               return this._selectable;
             },
             set: function selectable(value) {
               somewhatImplemented('TextField.selectable');
               this._selectable = value;
             }
           },
           wordWrap: {
             get: function wordWrap() {
-              return this._wordWrap;
+              return this._content.wordWrap;
             },
             set: function wordWrap(value) {
-              if (this._wordWrap === value) {
+              if (this._content.wordWrap === value) {
                 return;
               }
-              this._wordWrap = value;
+              this._content.wordWrap = value;
               this.invalidateDimensions();
             }
           },
           textHeight: {
             get: function textHeight() {
               this.ensureDimensions();
-              return this._textHeight;
+              return this._content.textHeight;
             }
           },
           textWidth: {
             get: function textWidth() {
               this.ensureDimensions();
-              return this._textWidth;
+              return this._content.textWidth;
             }
           },
           length: {
             get: function length() {
-              return this._content.text.length;
+              return this.text.length;
             }
           },
           numLines: {
             get: function numLines() {
               this.ensureDimensions();
-              return this._lines.length;
+              return this._content.lines.length;
             }
           },
           getLineMetrics: function (lineIndex) {
             return this.getLineMetrics(lineIndex);
           },
           setSelection: function (beginIndex, endIndex) {
             somewhatImplemented('TextField.setSelection');
           },
@@ -50230,35 +49948,36 @@ var TextFieldDefinition = function () {
           },
           bottomScrollV: {
             get: function bottomScrollV() {
               this.ensureDimensions();
               if (this._scrollV === 1) {
                 return this._bottomScrollV;
               }
               var maxVisibleY = (this._bbox.yMax - 80) / 20;
-              var offsetY = this._lines[this._scrollV - 1].y;
-              for (var i = this._bottomScrollV; i < this._lines.length; i++) {
-                var line = this._lines[i];
+              var lines = this._content.lines;
+              var offsetY = lines[this._scrollV - 1].y;
+              for (var i = this._bottomScrollV; i < lines.length; i++) {
+                var line = lines[i];
                 if (line.y + line.height + offsetY > maxVisibleY) {
                   return i + 1;
                 }
               }
             }
           },
           maxScrollV: {
             get: function maxScrollV() {
               this.ensureDimensions();
               return this._maxScrollV;
             }
           },
           maxScrollH: {
             get: function maxScrollH() {
               this.ensureDimensions();
-              return Math.max(this._textWidth - this._bbox.xMax / 20 + 4, 0);
+              return Math.max(this._content.textWidth - this._bbox.xMax / 20 + 4, 0);
             }
           },
           background: {
             get: function background() {
               return this._background;
             },
             set: function background(value) {
               if (this._background === value) {
@@ -50325,37 +50044,536 @@ var TextFieldDefinition = function () {
             },
             set: function embedFonts(value) {
               this.invalidateDimensions();
               this._embedFonts = value;
             }
           },
           condenseWhite: {
             get: function condenseWhite() {
-              return this._condenseWhite;
+              return this._content.condenseWhite;
             },
             set: function condenseWhite(value) {
               somewhatImplemented('TextField.condenseWhite');
-              this._condenseWhite = value;
+              this._content.condenseWhite = value;
             }
           },
           sharpness: {
             get: function sharpness() {
               return this._sharpness;
             },
             set: function sharpness(value) {
               somewhatImplemented('TextField.sharpness');
               this._sharpness = value;
             }
           }
         }
       }
     };
     return def;
   }.call(this);
+function TextFieldContent(initialFormat) {
+  this.defaultTextFormat = initialFormat;
+  this.textWidth = 0;
+  this.textHeight = 0;
+  this.condenseWhite = false;
+  this.wordWrap = false;
+  this.multiline = false;
+  this.textColor = null;
+  this._text = '';
+  this._htmlText = '';
+  this._createTrunk();
+  this._textRuns = null;
+  this._htmlParser = document.createElement('p');
+  this._measureCtx = document.createElement('canvas').getContext('2d');
+}
+TextFieldContent.knownNodeTypes = {
+  'BR': true,
+  'LI': true,
+  'P': true,
+  'B': true,
+  'I': true,
+  'FONT': true,
+  'TEXTFORMAT': true,
+  'U': true,
+  'A': true,
+  'IMG': true,
+  'SPAN': true
+};
+TextFieldContent.WRAP_OPPORTUNITIES = {
+  ' ': true,
+  '.': true,
+  '-': true,
+  '\t': true
+};
+TextFieldContent.TextLine = function (y) {
+  this.x = 0;
+  this.width = 0;
+  this.y = y;
+  this.height = 0;
+  this.leading = 0;
+  this.runs = [];
+  this.largestFormat = null;
+};
+TextFieldContent.prototype = {
+  get text() {
+    return this._text;
+  },
+  set text(val) {
+    val = val + '';
+    if (this._text === val) {
+      return;
+    }
+    var lines = [];
+    var lineOffset = 0;
+    for (var index = 0; index < val.length;) {
+      var char = val[index];
+      if (char === '\r' || char === '\n') {
+        lines.push(val.substring(lineOffset, index));
+        lineOffset = index;
+        if (char === '\r' && val[index + 1] === '\n') {
+          index++;
+        }
+      }
+      index++;
+    }
+    lines.push(val.substring(lineOffset, index));
+    this._createTrunk();
+    this._text = val;
+    this._htmlText = val;
+    this._tree.children[0].children[0] = {
+      type: 'plain-text',
+      lines: lines
+    };
+  },
+  get htmlText() {
+    return this._htmlText;
+  },
+  set htmlText(val) {
+    if (this._htmlText === val) {
+      return;
+    }
+    this.defaultTextFormat.bold = false;
+    this.defaultTextFormat.italic = false;
+    this._parseHtml(val);
+  },
+  calculateMetrics: function (bounds, embedFonts) {
+    var initialFormat = this.defaultTextFormat;
+    this.resolveFont(initialFormat, embedFonts);
+    this.lines = [];
+    this._textRuns = [
+      {
+        type: 'f',
+        format: initialFormat
+      }
+    ];
+    var width = Math.max(bounds.xMax / 20 - 4, 1);
+    var height = Math.max(bounds.yMax / 20 - 4, 1);
+    var state = {
+        ctx: this._measureCtx,
+        w: width,
+        h: height,
+        maxLineWidth: 0,
+        formats: [
+          initialFormat
+        ],
+        currentFormat: initialFormat,
+        line: new TextFieldContent.TextLine(0),
+        wordWrap: this.wordWrap,
+        combinedAlign: null,
+        textColor: this.textColor,
+        embedFonts: embedFonts
+      };
+    this._collectRuns(state, this._tree);
+    this._finishLine(state, false);
+    this.textWidth = state.maxLineWidth | 0;
+    this.textHeight = state.line.y | 0;
+    return state.combinedAlign;
+  },
+  makeFormatString: function (format) {
+    var boldItalic = '';
+    if (format.italic) {
+      boldItalic += 'italic';
+    }
+    if (format.bold) {
+      boldItalic += ' bold';
+    }
+    return boldItalic + ' ' + format.size + 'px ' + (format.font._uniqueName || format.font._fontName);
+  },
+  resolveFont: function (format, embedded) {
+    var face = format.face.toLowerCase();
+    if (face === '_sans') {
+      face = 'sans-serif';
+    } else if (face === '_serif') {
+      face = 'serif';
+    } else if (face === '_typewriter') {
+      face = 'monospace';
+    }
+    var style;
+    if (format.bold) {
+      if (format.italic) {
+        style = 'boldItalic';
+      } else {
+        style = 'bold';
+      }
+    } else if (format.italic) {
+      style = 'italic';
+    } else {
+      style = 'regular';
+    }
+    var font = FontDefinition.getFont(face, style, embedded);
+    format.font = font;
+  },
+  _parseHtml: function (val) {
+    this._htmlParser.innerHTML = val;
+    var rootElement = this._htmlParser.childNodes.length !== 1 ? this._htmlParser : this._htmlParser.childNodes[0];
+    this._text = '';
+    this._htmlText = val;
+    this._createTrunk();
+    if (rootElement.nodeType === 3) {
+      this._convertNode(rootElement, this._tree.children[0].children);
+    }
+    var initialNodeList = [
+        rootElement
+      ];
+    var attributes;
+    var format;
+    var key;
+    if (initialNodeList.length == 1 && rootElement.localName.toUpperCase() == 'P') {
+      attributes = this._extractAttributes(rootElement);
+      format = this._tree.format;
+      for (key in attributes) {
+        format[key] = attributes[key];
+      }
+      initialNodeList = rootElement.childNodes;
+      rootElement = rootElement.childNodes[0];
+    }
+    if (initialNodeList.length == 1 && rootElement.localName.toUpperCase() == 'FONT') {
+      attributes = this._extractAttributes(rootElement);
+      format = this._tree.children[0].format;
+      for (key in attributes) {
+        format[key] = attributes[key];
+      }
+      initialNodeList = rootElement.childNodes;
+    }
+    this._convertNodeList(initialNodeList, this._tree.children[0].children);
+  },
+  _createTrunk: function () {
+    var initialFormat = this.defaultTextFormat;
+    this._tree = {
+      type: 'SPAN',
+      format: {
+        ALIGN: initialFormat.align
+      },
+      children: []
+    };
+    var fontAttributes = {
+        FACE: initialFormat.face,
+        LETTERSPACING: initialFormat.letterSpacing,
+        KERNING: initialFormat.kerning,
+        LEADING: initialFormat.leading,
+        COLOR: initialFormat.color
+      };
+    this._tree.children[0] = {
+      type: 'FONT',
+      format: fontAttributes,
+      children: []
+    };
+  },
+  _convertNode: function (input, destinationList) {
+    if (!(input.nodeType === 1 || input.nodeType === 3) || input.prefix) {
+      return;
+    }
+    var node;
+    if (input.nodeType === 3) {
+      var text = input.textContent;
+      node = {
+        type: 'text',
+        text: text,
+        format: null,
+        children: null
+      };
+      this._text += text;
+      destinationList.push(node);
+      return;
+    }
+    var nodeType = input.localName.toUpperCase();
+    if (!TextFieldContent.knownNodeTypes[nodeType] || this.multiline === false && (nodeType === 'P' || nodeType === 'BR')) {
+      if (nodeType === 'SBR') {
+        destinationList.push({
+          type: 'BR',
+          text: null,
+          format: null,
+          children: null
+        });
+      }
+      this._convertNodeList(input.childNodes, destinationList);
+      return;
+    }
+    node = {
+      type: nodeType,
+      text: null,
+      format: this._extractAttributes(input),
+      children: []
+    };
+    this._convertNodeList(input.childNodes, node.children);
+    destinationList.push(node);
+  },
+  _convertNodeList: function (from, to) {
+    var childCount = from.length;
+    for (var i = 0; i < childCount; i++) {
+      this._convertNode(from[i], to);
+    }
+  },
+  _extractAttributes: function (node) {
+    var attributesList = node.attributes;
+    var attributesMap = {};
+    for (var i = 0; i < attributesList.length; i++) {
+      var attr = attributesList[i];
+      if (attr.prefix) {
+        continue;
+      }
+      attributesMap[attr.localName.toUpperCase()] = attr.value;
+    }
+    return attributesMap;
+  },
+  _collectRuns: function (state, node) {
+    var formatNode = false;
+    var blockNode = false;
+    switch (node.type) {
+    case 'plain-text':
+      var lines = node.lines;
+      for (var i = 0; i < lines.length; i++) {
+        this._addRunsForText(state, lines[i]);
+        if (i < lines.length - 1) {
+          this._finishLine(state, true);
+        }
+      }
+      return;
+    case 'text':
+      this._addRunsForText(state, node.text);
+      return;
+    case 'BR':
+      this._finishLine(state, true);
+      return;
+    case 'LI':
+    case 'P':
+      this._finishLine(state, false);
+      this._pushFormat(state, node);
+      blockNode = true;
+      break;
+    case 'B':
+    case 'I':
+    case 'FONT':
+    case 'TEXTFORMAT':
+      this._pushFormat(state, node);
+      formatNode = true;
+      break;
+    case 'U':
+    case 'A':
+    case 'IMG':
+    case 'SPAN':
+    default:
+    }
+    for (var i = 0; i < node.children.length; i++) {
+      var child = node.children[i];
+      this._collectRuns(state, child);
+    }
+    if (formatNode) {
+      this._popFormat(state);
+    }
+    if (blockNode) {
+      this._finishLine(state, true);
+    }
+  },
+  _addRunsForText: function (state, text) {
+    if (!text) {
+      return;
+    }
+    if (!state.wordWrap) {
+      this._addTextRun(state, text, state.ctx.measureText(text).width);
+      return;
+    }
+    while (text.length) {
+      var width = state.ctx.measureText(text).width;
+      var availableWidth = state.w - state.line.width;
+      if (availableWidth <= 0) {
+        this._finishLine(state, false);
+        availableWidth = state.w - state.line.width;
+      }
+      if (width <= availableWidth) {
+        this._addTextRun(state, text, width);
+        break;
+      } else {
+        var offset = text.length / width * availableWidth | 0;
+        while (state.ctx.measureText(text.substr(0, offset)).width < availableWidth && offset < text.length) {
+          offset++;
+        }
+        var wrapOffset = offset;
+        while (wrapOffset > -1) {
+          if (TextFieldContent.WRAP_OPPORTUNITIES[text[wrapOffset]]) {
+            wrapOffset++;
+            break;
+          }
+          wrapOffset--;
+        }
+        if (wrapOffset === -1) {
+          if (state.line.width > 0) {
+            this._finishLine(state, false);
+            continue;
+          }
+          while (state.ctx.measureText(text.substr(0, offset)).width > availableWidth) {
+            offset--;
+          }
+          if (offset === 0) {
+            offset = 1;
+          }
+          wrapOffset = offset;
+        }
+        var runText = text.substr(0, wrapOffset);
+        width = state.ctx.measureText(runText).width;
+        this._addTextRun(state, runText, width);
+        if (state.wordWrap) {
+          this._finishLine(state, false);
+        }
+        text = text.substr(wrapOffset);
+      }
+    }
+  },
+  _addTextRun: function (state, text, width) {
+    if (text.length === 0) {
+      return;
+    }
+    var line = state.line;
+    var format = state.currentFormat;
+    var size = format.size;
+    var run = {
+        type: 't',
+        text: text,
+        x: line.width
+      };
+    this._textRuns.push(run);
+    state.line.runs.push(run);
+    line.width += width | 0;
+    if (line.leading === 0 && format.leading > line.leading) {
+      line.leading = format.leading;
+    }
+    if (!line.largestFormat || size > line.largestFormat.size) {
+      line.largestFormat = format;
+    }
+  },
+  _finishLine: function (state, forceNewline) {
+    var line = state.line;
+    if (line.runs.length === 0) {
+      if (forceNewline) {
+        var format = state.currentFormat;
+        state.line.y += format.font._metrics.height * format.size + format.leading | 0;
+      }
+      return;
+    }
+    var runs = line.runs;
+    var format = line.largestFormat;
+    var baselinePos = line.y + format.font._metrics.ascent * format.size;
+    for (var i = runs.length; i--;) {
+      runs[i].y = baselinePos;
+    }
+    var align = (state.currentFormat.align || '').toLowerCase();
+    if (state.combinedAlign === null) {
+      state.combinedAlign = align;
+    } else if (state.combinedAlign !== align) {
+      state.combinedAlign = 'mixed';
+    }
+    if (align === 'center' || align === 'right') {
+      var offset = Math.max(state.w - line.width, 0);
+      if (align === 'center') {
+        offset >>= 1;
+      }
+      for (i = runs.length; i--;) {
+        runs[i].x += offset;
+      }
+    }
+    line.height = format.font._metrics.height * format.size + line.leading | 0;
+    state.maxLineWidth = Math.max(state.maxLineWidth, line.width);
+    this.lines.push(line);
+    state.line = new TextFieldContent.TextLine(line.y + line.height);
+  },
+  _pushFormat: function (state, node) {
+    var attributes = node.format;
+    var format = Object.create(state.formats[state.formats.length - 1]);
+    var fontChanged = false;
+    switch (node.type) {
+    case 'P':
+      if (attributes.ALIGN === format.align) {
+        return;
+      }
+      format.align = attributes.ALIGN;
+      break;
+    case 'B':
+      format.bold = true;
+      fontChanged = true;
+      break;
+    case 'I':
+      format.italic = true;
+      fontChanged = true;
+      break;
+    case 'FONT':
+      if (attributes.COLOR !== undefined) {
+        format.color = attributes.COLOR;
+      }
+      if (attributes.FACE !== undefined) {
+        format.face = attributes.FACE;
+        fontChanged = true;
+      }
+      if (attributes.SIZE !== undefined) {
+        format.size = parseFloat(attributes.SIZE);
+      }
+      if (attributes.LETTERSPACING !== undefined) {
+        format.letterspacing = parseFloat(attributes.LETTERSPACING);
+      }
+      if (attributes.KERNING !== undefined) {
+        format.kerning = attributes.KERNING && true;
+      }
+    case 'TEXTFORMAT':
+      if (attributes.LEADING !== undefined) {
+        format.leading = parseFloat(attributes.LEADING);
+      }
+      if (attributes.INDENT !== undefined) {
+        state.line.x = attributes.INDENT;
+        state.line.width += attributes.INDENT | 0;
+      }
+      break;
+    default:
+      warning('Unknown format node encountered: ' + node.type);
+      return;
+    }
+    if (state.textColor !== null) {
+      format.color = rgbIntAlphaToStr(state.textColor, 1);
+    }
+    if (fontChanged) {
+      this.resolveFont(format, state.embedFonts);
+    }
+    format.str = this.makeFormatString(format);
+    state.formats.push(format);
+    this._textRuns.push({
+      type: 'f',
+      format: format
+    });
+    state.currentFormat = format;
+    state.ctx.font = format.str;
+  },
+  _popFormat: function (state) {
+    state.formats.pop();
+    var format = state.currentFormat = state.formats[state.formats.length - 1];
+    this._textRuns.push({
+      type: 'f',
+      format: format
+    });
+    state.ctx.font = state.str;
+  }
+};
 var TextFormatDefinition = function () {
     var measureTextField;
     return {
       __class__: 'flash.text.TextFormat',
       initialize: function () {
       },
       fromObject: function (obj) {
         this._font = obj.face || null;
--- a/browser/extensions/shumway/content/version.txt
+++ b/browser/extensions/shumway/content/version.txt
@@ -1,1 +1,2 @@
-0.7.933
+0.8.6
+4728574
--- a/browser/extensions/shumway/content/web/avm-sandbox.js
+++ b/browser/extensions/shumway/content/web/avm-sandbox.js
@@ -103,48 +103,74 @@ function runViewer() {
       var fid = s.split('/')[0];
       return fid !== '5' && fid !== '34' && fid !== '35'; // more?
     }).join(',');
   }
 
   parseSwf(movieUrl, movieParams, objectParams);
 
   if (isOverlay) {
+    document.getElementById('overlay').className = 'enabled';
     var fallbackDiv = document.getElementById('fallback');
-    fallbackDiv.className = 'enabled';
     fallbackDiv.addEventListener('click', function(e) {
       fallback();
       e.preventDefault();
     });
+    var reportDiv = document.getElementById('report');
+    reportDiv.addEventListener('click', function(e) {
+      reportIssue();
+      e.preventDefault();
+    });
     var fallbackMenu = document.getElementById('fallbackMenu');
     fallbackMenu.removeAttribute('hidden');
     fallbackMenu.addEventListener('click', fallback);
   }
   var showURLMenu = document.getElementById('showURLMenu');
   showURLMenu.addEventListener('click', showURL);
   var inspectorMenu = document.getElementById('inspectorMenu');
   inspectorMenu.addEventListener('click', showInInspector);
+  var reportMenu = document.getElementById('reportMenu');
+  reportMenu.addEventListener('click', reportIssue);
 
   document.getElementById('copyProfileMenu').addEventListener('click', copyProfile);
 }
 
 function showURL() {
-  var flashParams = JSON.parse(FirefoxCom.requestSync('getPluginParams', null));
-  window.prompt("Copy to clipboard", flashParams.url);
+  window.prompt("Copy to clipboard", movieUrl);
 }
 
 function showInInspector() {
   var base = "http://www.areweflashyet.com/shumway/examples/inspector/inspector.html?rfile=";
   var params = '';
   for (var k in movieParams) {
     params += '&' + k + '=' + encodeURIComponent(movieParams[k]);
   }
   window.open(base + encodeURIComponent(movieUrl) + params);
 }
 
+function reportIssue() {
+  var duplicatesMap = Object.create(null);
+  var prunedExceptions = [];
+  avm2.exceptions.forEach(function(e) {
+    var ident = e.source + e.message + e.stack;
+    var entry = duplicatesMap[ident];
+    if (!entry) {
+      entry = duplicatesMap[ident] = {
+        source: e.source,
+        message: e.message,
+        stack: e.stack,
+        count: 0
+      };
+      prunedExceptions.push(entry);
+    }
+    entry.count++;
+  });
+  FirefoxCom.requestSync('reportIssue', JSON.stringify(prunedExceptions));
+}
+
 function copyProfile() {
   function toArray(v) {
     var array = [];
     for (var i = 0; i < v.length; i++) {
       array.push(v[i]);
     }
     return array;
   }
@@ -231,16 +257,22 @@ var FileLoadingService = {
   }
 };
 
 function parseSwf(url, movieParams, objectParams) {
   var compilerSettings = JSON.parse(
     FirefoxCom.requestSync('getCompilerSettings', null));
   enableVerifier.value = compilerSettings.verifier;
 
+  // init misc preferences
+  turboMode.value = FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.turboMode', def: false});
+  hud.value = FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.hud', def: false});
+  forceHidpi.value = FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.force_hidpi', def: false});
+  dummyAnimation.value = FirefoxCom.requestSync('getBoolPref', {pref: 'shumway.dummyMode', def: false});
+
   console.log("Compiler settings: " + JSON.stringify(compilerSettings));
   console.log("Parsing " + url + "...");
   function loaded() {
     FirefoxCom.request('endActivation', null);
   }
 
   createAVM2(builtinPath, playerGlobalPath, avm1Path,
     compilerSettings.sysCompiler ? EXECUTION_MODE.COMPILE : EXECUTION_MODE.INTERPRET,
--- a/browser/extensions/shumway/content/web/viewer.html
+++ b/browser/extensions/shumway/content/web/viewer.html
@@ -33,59 +33,75 @@ limitations under the License.
 
       #viewer {
         position:fixed !important;
         left:0;top:0;bottom:0;right:0;
         overflow: hidden;
         line-height: 0;
       }
 
-      #fallback {
+      #overlay {
         display: none;
       }
 
-      #fallback.enabled {
+      #overlay.enabled {
         display: block;
         position:fixed;
+        bottom: 0;
+        right: 0;
+      }
+
+      #report, #fallback {
+        float: right;
+        width: 70px; height: 16px;
+        padding: 8px 4px 4px;
         color: white;
-        right: 0px; bottom: 0px; width: 70px; height: 16px;
-        padding: 4px;
-        padding-top: 8px;
         background-color: rgba(0, 0, 0, 0.62);
         font: bold 10px sans-serif;
         text-align: center;
         text-decoration: none;
       }
+      #report {
+        display: none;
+        width: 100px;
+      }
+      #overlay:hover #report {
+        display: block;
+      }
 
       #fallback .icon {
         display: none;
         color: white;
       }
 
-      #fallback.enabled:hover .icon {
+      #fallback:hover .icon {
         display: inline-block;
       }
 
-      #fallback:hover {
-        background-color: rgb(0, 0, 0);
+      #report:hover, #fallback:hover {
+        background-color: black;
       }
 
       @media screen and (max-width: 100px), screen and (max-height: 40px) {
-        body.started #fallback {
+        body.started #overlay {
           display: none;
         }
       }
     </style>
   </head>
 
   <body contextmenu="shumwayMenu">
     <div id="viewer"></div>
     <section>
-      <a id="fallback" href="#">Shumway <span class="icon">&times;</span></a>
+      <div id="overlay">
+        <a id="fallback" href="#">Shumway <span class="icon">&times;</span></a>
+        <a id="report" href="#">Report Problems</a>
+      </div>
       <menu type="context" id="shumwayMenu">
         <menuitem label="Show URL" id="showURLMenu"></menuitem>
         <menuitem label="Copy Profile" id="copyProfileMenu"></menuitem>
         <menuitem label="Open in Inspector" id="inspectorMenu"></menuitem>
+        <menuitem label="Report Problems" id="reportMenu"></menuitem>
         <menuitem label="Fallback to Flash" id="fallbackMenu" hidden></menuitem>
       </menu>
     </section>
   </body>
 </html>