betajs/betajs-media

View on GitHub
src/encoding/webm_encoder.js

Summary

Maintainability
C
7 hrs
Test Coverage
Scoped.define("module:Encoding.WebmEncoder.Support", [
    "base:Promise",
    "base:Scheduling.Helper",
    "base:Types"
], function(Promise, SchedulingHelper, Types) {
    return {

        parseWebP: function(riff) {
            var VP8 = riff.RIFF[0].WEBP[0];

            var frame_start = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
            for (var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i);

            var width, height, tmp;

            //the code below is literally copied verbatim from the bitstream spec
            tmp = (c[1] << 8) | c[0];
            width = tmp & 0x3FFF;
            tmp = (c[3] << 8) | c[2];
            height = tmp & 0x3FFF;
            return {
                width: width,
                height: height,
                data: VP8,
                riff: riff
            };
        },

        parseRIFF: function(string) {
            var offset = 0;
            var chunks = {};

            var f = function(i) {
                var unpadded = i.charCodeAt(0).toString(2);
                return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
            };

            while (offset < string.length) {
                var id = string.substr(offset, 4);
                var len = parseInt(string.substr(offset + 4, 4).split('').map(f).join(''), 2);
                var data = string.substr(offset + 4 + 4, len);
                offset += 4 + 4 + len;
                chunks[id] = chunks[id] || [];

                if (id == 'RIFF' || id == 'LIST')
                    chunks[id].push(this.parseRIFF(data));
                else
                    chunks[id].push(data);
            }
            return chunks;
        },

        /**
         *
         * @param {HTMLCanvasElement} canvas
         * @param {object} frame
         * @param {number} _pixTolerance - 0 - only black pixel color; 1 - all
         * @param {number} _frameTolerance - 0 - only black frame color; 1 - all
         * @return {boolean}
         */
        isBlankFrame: function(canvas, frame, _pixTolerance, _frameTolerance) {
            if (typeof Array.prototype.some === "function") {
                return !canvas.getContext('2d')
                    .getImageData(0, 0, canvas.width, canvas.height).data
                    .some(function(channel) {
                        return channel !== 0;
                    });
            } else {
                var localCanvas = document.createElement('canvas');
                localCanvas.width = canvas.width;
                localCanvas.height = canvas.height;
                var context2d = localCanvas.getContext('2d');

                var sampleColor = {
                    r: 0,
                    g: 0,
                    b: 0
                };
                var maxColorDifference = Math.sqrt(
                    Math.pow(255, 2) +
                    Math.pow(255, 2) +
                    Math.pow(255, 2)
                );
                var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0;
                var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0;

                var matchPixCount, endPixCheck, maxPixCount;

                var image = new Image();
                image.src = frame.image;
                context2d.drawImage(image, 0, 0, canvas.width, canvas.height);
                var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height);
                matchPixCount = 0;
                endPixCheck = imageData.data.length;
                maxPixCount = imageData.data.length / 4;

                for (var pix = 0; pix < endPixCheck; pix += 4) {
                    var currentColor = {
                        r: imageData.data[pix],
                        g: imageData.data[pix + 1],
                        b: imageData.data[pix + 2]
                    };
                    var colorDifference = Math.sqrt(
                        Math.pow(currentColor.r - sampleColor.r, 2) +
                        Math.pow(currentColor.g - sampleColor.g, 2) +
                        Math.pow(currentColor.b - sampleColor.b, 2)
                    );
                    if (colorDifference <= maxColorDifference * pixTolerance)
                        matchPixCount++;
                }

                return maxPixCount - matchPixCount <= maxPixCount * frameTolerance;
            }
        },

        makeSimpleBlock: function(data) {
            var flags = 0;
            if (data.keyframe) flags |= 128;
            if (data.invisible) flags |= 8;
            if (data.lacing) flags |= (data.lacing << 1);
            if (data.discardable) flags |= 1;
            if (data.trackNum > 127)
                throw "TrackNumber > 127 not supported";
            var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) {
                return String.fromCharCode(e);
            }).join('') + data.frame;
            return out;
        },

        generateEBMLHeader: function(duration, width, height) {

            var doubleToString = function(num) {
                return [].slice.call(
                    new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {
                    return String.fromCharCode(e);
                }).reverse().join('');
            };

            return [{
                "id": 0x1a45dfa3, // EBML
                "data": [{
                    "data": 1,
                    "id": 0x4286 // EBMLVersion
                }, {
                    "data": 1,
                    "id": 0x42f7 // EBMLReadVersion
                }, {
                    "data": 4,
                    "id": 0x42f2 // EBMLMaxIDLength
                }, {
                    "data": 8,
                    "id": 0x42f3 // EBMLMaxSizeLength
                }, {
                    "data": "webm",
                    "id": 0x4282 // DocType
                }, {
                    "data": 2,
                    "id": 0x4287 // DocTypeVersion
                }, {
                    "data": 2,
                    "id": 0x4285 // DocTypeReadVersion
                }]
            }, {
                "id": 0x18538067, // Segment
                "data": [{
                    "id": 0x1549a966, // Info
                    "data": [{
                        "data": 1e6, //do things in millisecs (num of nanosecs for duration scale)
                        "id": 0x2ad7b1 // TimecodeScale
                    }, {
                        "data": "whammy",
                        "id": 0x4d80 // MuxingApp
                    }, {
                        "data": "whammy",
                        "id": 0x5741 // WritingApp
                    }, {
                        "data": doubleToString(duration),
                        "id": 0x4489 // Duration
                    }]
                }, {
                    "id": 0x1654ae6b, // Tracks
                    "data": [{
                        "id": 0xae, // TrackEntry
                        "data": [{
                            "data": 1,
                            "id": 0xd7 // TrackNumber
                        }, {
                            "data": 1,
                            "id": 0x63c5 // TrackUID
                        }, {
                            "data": 0,
                            "id": 0x9c // FlagLacing
                        }, {
                            "data": "und",
                            "id": 0x22b59c // Language
                        }, {
                            "data": "V_VP8",
                            "id": 0x86 // CodecID
                        }, {
                            "data": "VP8",
                            "id": 0x258688 // CodecName
                        }, {
                            "data": 1,
                            "id": 0x83 // TrackType
                        }, {
                            "id": 0xe0, // Video
                            "data": [{
                                "data": width,
                                "id": 0xb0 // PixelWidth
                            }, {
                                "data": height,
                                "id": 0xba // PixelHeight
                            }]
                        }]
                    }]
                }]
            }];
        },

        __numToBuffer: function(num) {
            var parts = [];
            while (num > 0) {
                parts.push(num & 0xff);
                num = num >> 8;
            }
            return new Uint8Array(parts.reverse());
        },

        __strToBuffer: function(str) {
            return new Uint8Array(str.split('').map(function(e) {
                return e.charCodeAt(0);
            }));
        },

        __bitsToBuffer: function(bits) {
            var data = [];
            var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
            bits = pad + bits;
            for (var i = 0; i < bits.length; i += 8)
                data.push(parseInt(bits.substr(i, 8), 2));
            return new Uint8Array(data);
        },

        serializeEBML: function(json) {
            if (!Types.is_array(json))
                return json;
            var ebml = [];
            json.forEach(function(entry) {
                var data = entry.data;
                var tp = typeof data;
                if (tp === "object")
                    data = this.serializeEBML(data);
                else if (tp === "number")
                    data = this.__bitsToBuffer(data.toString(2));
                else if (tp === "string")
                    data = this.__strToBuffer(data);
                var len = data.size || data.byteLength || data.length;
                var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
                var size_str = len.toString(2);
                var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str;
                var size = (new Array(zeroes)).join('0') + '1' + padded;
                ebml.push(this.__numToBuffer(entry.id));
                ebml.push(this.__bitsToBuffer(size));
                ebml.push(data);
            }, this);
            return new Blob(ebml, {
                type: "video/webm"
            });
        },

        makeTimecodeDataBlock: function(data, clusterDuration) {
            return {
                data: this.makeSimpleBlock({
                    discardable: 0,
                    frame: data.slice(4),
                    invisible: 0,
                    keyframe: 1,
                    lacing: 0,
                    trackNum: 1,
                    timecode: Math.round(clusterDuration)
                }),
                id: 0xa3
            };
        },

        makeCluster: function(clusterFrames, clusterTimecode) {
            clusterFrames.unshift({
                "data": clusterTimecode,
                "id": 0xe7 // Timecode
            });
            return {
                "id": 0x1f43b675, // Cluster
                "data": clusterFrames
            };
        }

    };
});