From 546f7139b35b36fb4dc8e299094c8a026caa9733 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 14 Sep 2015 18:58:59 -0500 Subject: [PATCH] Creation and playback of compressed SWFM files. --- src/base/dataBuffer.ts | 8 +- src/gfx/test/recorder.ts | 72 +++++++++++++++-- utils/compressswfm.js | 162 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 utils/compressswfm.js diff --git a/src/base/dataBuffer.ts b/src/base/dataBuffer.ts index 94822347c2..24e34dbb31 100644 --- a/src/base/dataBuffer.ts +++ b/src/base/dataBuffer.ts @@ -166,8 +166,12 @@ module Shumway.ArrayUtilities { * will be set to 0. */ compact(): void { - this._u8.set(this._u8.subarray(this._position, this._length), 0); - this._length -= this._position; + var position = this._position; + if (position === 0) { + return; // nothing to compact + } + this._u8.set(this._u8.subarray(position, this._length), 0); + this._length -= position; this._position = 0; } diff --git a/src/gfx/test/recorder.ts b/src/gfx/test/recorder.ts index 96a2d4a574..f99f16f84e 100644 --- a/src/gfx/test/recorder.ts +++ b/src/gfx/test/recorder.ts @@ -17,6 +17,9 @@ module Shumway.GFX.Test { import DataBuffer = Shumway.ArrayUtilities.DataBuffer; import PlainObjectDataBuffer = Shumway.ArrayUtilities.PlainObjectDataBuffer; + import IDataDecoder = Shumway.ArrayUtilities.IDataDecoder; + import Inflate = Shumway.ArrayUtilities.Inflate; + import LzmaDecoder = Shumway.ArrayUtilities.LzmaDecoder; enum MovieRecordObjectType { Undefined = 0, @@ -299,6 +302,7 @@ module Shumway.GFX.Test { private _state: MovieRecordParserState; private _comressionType: MovieCompressionType; private _closed: boolean; + private _transform: IDataDecoder; public currentTimestamp: number; public currentType: MovieRecordType; @@ -310,7 +314,51 @@ module Shumway.GFX.Test { this._closed = false; } - public push(data: Uint8Array){ + public push(data: Uint8Array): void { + if (this._state === MovieRecordParserState.Initial) { + // SWFM file starts from 4 bytes header: "MSWF", "MSWC", or "MSWZ" + var needToRead = MovieHeaderSize - this._buffer.length; + this._buffer.writeRawBytes(data.subarray(0, needToRead)); + if (MovieHeaderSize > this._buffer.length) { + return; + } + this._buffer.position = 0; + var headerBytes = this._buffer.readRawBytes(MovieHeaderSize); + if (headerBytes[0] !== 0x4D || headerBytes[1] !== 0x53 || headerBytes[2] !== 0x57 || + (headerBytes[3] !== 0x46 && headerBytes[3] !== 0x43 && headerBytes[3] !== 0x5A)) { + console.warn('Invalid SWFM header, stopping parsing'); + return; + } + switch (headerBytes[3]) { + case 0x46: // 'F' + this._comressionType = MovieCompressionType.None; + this._transform = { + push: function (data: Uint8Array) { this.onData(data); }, + close: function () {}, + onData: null, + onError: null + }; + break; + case 0x43: // 'C' + this._comressionType = MovieCompressionType.ZLib; + this._transform = Inflate.create(true); + break; + case 0x5A: // 'Z' + this._comressionType = MovieCompressionType.Lzma; + this._transform = new LzmaDecoder(false); + break; + } + + this._transform.onData = this._pushTransformed.bind(this); + this._transform.onError = this._errorTransformed.bind(this); + this._state = MovieRecordParserState.Parsing; + this._buffer.clear(); + data = data.subarray(needToRead); + } + this._transform.push(data); + } + + private _pushTransformed(data: Uint8Array): void { this._buffer.compact(); var savedPosition = this._buffer.position; @@ -320,21 +368,29 @@ module Shumway.GFX.Test { } public close() { + this._transform.close(); this._closed = true; } + private _errorTransformed() { + console.warn('Error in SWFM stream'); + this.close(); + } + public readNextRecord(): MovieRecordType { if (this._state === MovieRecordParserState.Initial) { - if (this._buffer.position + MovieHeaderSize > this._buffer.length) { - return MovieRecordType.Incomplete; - } - this._buffer.position += MovieHeaderSize; - this._comressionType = MovieCompressionType.None; - this._state = MovieRecordParserState.Parsing; + return MovieRecordType.Incomplete; + } + if (this._state === MovieRecordParserState.Ended) { + return MovieRecordType.None; } if (this._buffer.position >= this._buffer.length) { - return this._closed ? MovieRecordType.None : MovieRecordType.Incomplete; + if (this._closed) { + this._state = MovieRecordParserState.Ended; + return MovieRecordType.None; + } + return MovieRecordType.Incomplete; } if (this._buffer.position + MovieRecordHeaderSize > this._buffer.length) { diff --git a/utils/compressswfm.js b/utils/compressswfm.js new file mode 100644 index 0000000000..23102aeb24 --- /dev/null +++ b/utils/compressswfm.js @@ -0,0 +1,162 @@ +/* + * Copyright 2015 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var fs = require('fs'); +var path = require('path'); +var temp = require('temp'); +var spawn = require('child_process').spawn; + +// simple args parsing +var compressMethod = null; +var outputPath = null; +var inputPath = null; +for (var i = 2; i < process.argv.length;) { + var cmd = process.argv[i++]; + switch (cmd) { + case '--lzma': + case '-z': + compressMethod = 'lzma'; + break; + case '--zlib': + case '-c': + compressMethod = 'zlib'; + break; + case '--out': + case '-o': + outputPath = process.argv[i++]; + break; + default: + inputPath = cmd; // .swfm is expected + break; + } +} + +function createRawSWFM(callback) { + // Create a temp file without 'MSWF' header. + fs.readFile(inputPath, function (err, data) { + if (err) return callback(err); + var header = data.slice(0, 4).toString(); + if (header !== 'MSWF') { + return callback(new Error('swfm file header is not found: found \"' + header + '\"')); + } + temp.open('swfm', function (err, info) { + if (err) return callback(err); + fs.write(info.fd, data, 4, data.length - 4, function (err) { + if (err) return callback(err); + fs.close(info.fd, function (err) { + if (err) return callback(err); + callback(null, info.path); + }); + }); + }); + }); +} + +function compressViaGZip(rawDataPath, compressedDataPath, callback) { + // Running gzip. + var proc = spawn('gzip', ['-k9n', rawDataPath]); + var resultPath = rawDataPath + '.gz'; + proc.on('close', function (code) { + if (code !== 0 || !fs.existsSync(resultPath)) { + callback(new Error('Unable to run gzip')); + return; + } + // Prepending MSWC and zlib header before compressed data. + fs.writeFile(compressedDataPath, new Buffer("MSWC\u0078\u00DA", 'ascii'), function (err) { + if (err) return callback(err); + fs.readFile(resultPath, function (err, data) { + if (data[0] !== 0x1F || data[1] !== 0x8B || data[2] !== 0x08 || data[3] !== 0) { + return callback(new Error('Invalid gzip result')); + } + var dataStart = 10; + var dataEnd = data.length - 8; + + // Appending adler32 at the end of data. + var a = 1, b = 0; + for (var i = dataStart; i < dataEnd; ++i) { + a = (a + (data[i] & 0xff)) % 65521; + b = (b + a) % 65521; + } + var adler32 = (b << 16) | a; + data[dataEnd] = (adler32 >> 24) & 255; + data[dataEnd + 1] = (adler32 >> 16) & 255; + data[dataEnd + 2] = (adler32 >> 8) & 255; + data[dataEnd + 3] = adler32 & 255; + + + if (err) return callback(err); + fs.appendFile(compressedDataPath, data.slice(dataStart, dataEnd + 4), function (err) { + fs.unlink(resultPath, callback); + }); + }); + }); + }); +} + +function compressViaLzma(rawDataPath, compressedDataPath, callback) { + // Running lzma. + var proc = spawn('lzma', ['-zk9e', rawDataPath]); + var resultPath = rawDataPath + '.lzma'; + proc.on('close', function (code) { + if (code !== 0 || !fs.existsSync(resultPath)) { + callback(new Error('Unable to run lzma')); + return; + } + // Prepending MSWZ before lzma compressed data. + fs.writeFile(compressedDataPath, "MSWZ", function (err) { + if (err) return callback(err); + fs.readFile(resultPath, function (err, data) { + if (err) return callback(err); + fs.appendFile(compressedDataPath, data, function (err) { + fs.unlink(resultPath, callback); + }); + }); + }); + }); +} + +function printUsage() { + console.info('Usage: node compressswfm.js [-c|-z] inputFile [-o outputFile]'); +} + +if (!inputPath || !compressMethod) { + printUsage(); + process.exit(1); +} + +if (!outputPath) { + var ext = path.extname(inputPath); + outputPath = inputPath.slice(0, -ext.length) + '.' + compressMethod + ext; + console.log('Compressed results at ' + outputPath); +} + +createRawSWFM(function (err, path) { + if (err) throw err; + switch (compressMethod) { + case 'zlib': + compressViaGZip(path, outputPath, function (err) { + if (err) throw err; + console.log('Done. Compressed using ZLib.'); + }); + break; + case 'lzma': + compressViaLzma(path, outputPath, function (err) { + if (err) throw err; + console.log('Done. Compressed using LZMA.'); + }); + break; + } +});