/* * Copyright (C) 2016 Bilibili. All Rights Reserved. * * @author zheng qian * * 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. */ import Log from '../utils/logger.js'; import decodeUTF8 from '../utils/utf8-conv.js'; import {IllegalStateException} from '../utils/exception.js'; let le = (function () { let buf = new ArrayBuffer(2); (new DataView(buf)).setInt16(0, 256, true); // little-endian write return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE })(); class AMF { static parseScriptData(arrayBuffer, dataOffset, dataSize) { let data = {}; try { let name = AMF.parseValue(arrayBuffer, dataOffset, dataSize); let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size); data[name.data] = value.data; } catch (e) { Log.e('AMF', e.toString()); } return data; } static parseObject(arrayBuffer, dataOffset, dataSize) { if (dataSize < 3) { throw new IllegalStateException('Data not enough when parse ScriptDataObject'); } let name = AMF.parseString(arrayBuffer, dataOffset, dataSize); let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size); let isObjectEnd = value.objectEnd; return { data: { name: name.data, value: value.data }, size: name.size + value.size, objectEnd: isObjectEnd }; } static parseVariable(arrayBuffer, dataOffset, dataSize) { return AMF.parseObject(arrayBuffer, dataOffset, dataSize); } static parseString(arrayBuffer, dataOffset, dataSize) { if (dataSize < 2) { throw new IllegalStateException('Data not enough when parse String'); } let v = new DataView(arrayBuffer, dataOffset, dataSize); let length = v.getUint16(0, !le); let str; if (length > 0) { str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 2, length)); } else { str = ''; } return { data: str, size: 2 + length }; } static parseLongString(arrayBuffer, dataOffset, dataSize) { if (dataSize < 4) { throw new IllegalStateException('Data not enough when parse LongString'); } let v = new DataView(arrayBuffer, dataOffset, dataSize); let length = v.getUint32(0, !le); let str; if (length > 0) { str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 4, length)); } else { str = ''; } return { data: str, size: 4 + length }; } static parseDate(arrayBuffer, dataOffset, dataSize) { if (dataSize < 10) { throw new IllegalStateException('Data size invalid when parse Date'); } let v = new DataView(arrayBuffer, dataOffset, dataSize); let timestamp = v.getFloat64(0, !le); let localTimeOffset = v.getInt16(8, !le); timestamp += localTimeOffset * 60 * 1000; // get UTC time return { data: new Date(timestamp), size: 8 + 2 }; } static parseValue(arrayBuffer, dataOffset, dataSize) { if (dataSize < 1) { throw new IllegalStateException('Data not enough when parse Value'); } let v = new DataView(arrayBuffer, dataOffset, dataSize); let offset = 1; let type = v.getUint8(0); let value; let objectEnd = false; try { switch (type) { case 0: // Number(Double) type value = v.getFloat64(1, !le); offset += 8; break; case 1: { // Boolean type let b = v.getUint8(1); value = b ? true : false; offset += 1; break; } case 2: { // String type let amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1); value = amfstr.data; offset += amfstr.size; break; } case 3: { // Object(s) type value = {}; let terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) { terminal = 3; } while (offset < dataSize - 4) { // 4 === type(UI8) + ScriptDataObjectEnd(UI24) let amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal); if (amfobj.objectEnd) break; value[amfobj.data.name] = amfobj.data.value; offset += amfobj.size; } if (offset <= dataSize - 3) { let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; if (marker === 9) { offset += 3; } } break; } case 8: { // ECMA array type (Mixed array) value = {}; offset += 4; // ECMAArrayLength(UI32) let terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) { terminal = 3; } while (offset < dataSize - 8) { // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24) let amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - terminal); if (amfvar.objectEnd) break; value[amfvar.data.name] = amfvar.data.value; offset += amfvar.size; } if (offset <= dataSize - 3) { let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF; if (marker === 9) { offset += 3; } } break; } case 9: // ScriptDataObjectEnd value = undefined; offset = 1; objectEnd = true; break; case 10: { // Strict array type // ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf value = []; let strictArrayLength = v.getUint32(1, !le); offset += 4; for (let i = 0; i < strictArrayLength; i++) { let val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset); value.push(val.data); offset += val.size; } break; } case 11: { // Date type let date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1); value = date.data; offset += date.size; break; } case 12: { // Long string type let amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1); value = amfLongStr.data; offset += amfLongStr.size; break; } default: // ignore and skip offset = dataSize; Log.w('AMF', 'Unsupported AMF value type ' + type); } } catch (e) { Log.e('AMF', e.toString()); } return { data: value, size: offset, objectEnd: objectEnd }; } } export default AMF;