UNPKG

webm-duration-fix

Version:

based on ts-ebml and support large file(than 2GB) and optimize memory usage during repair

704 lines (628 loc) 28.4 kB
/// <reference types="node"/> import {Int64BE, Uint64BE} from "int64-buffer"; import * as EBML from "./EBML"; import Encoder from "./EBMLEncoder"; import * as _Buffer from "buffer"; import _tools from "./tools-ebml"; import * as _block from "ebml-block"; export const Buffer: typeof global.Buffer = _Buffer.Buffer; export const readVint: (buffer: Buffer, start: number)=> null | ({length: number; value: number; }) = _tools.readVint; export const writeVint: (val: number)=> Buffer = _tools.writeVint; export const ebmlBlock: (buf: Buffer)=> EBML.SimpleBlock = _block; export function readBlock(buf: ArrayBuffer): EBML.SimpleBlock { return ebmlBlock(new Buffer(buf)); } /** * @param end - if end === false then length is unknown */ export function encodeTag(tagId: Buffer, tagData: Buffer, unknownSize=false): Buffer { return concat([ tagId, unknownSize ? new Buffer('01ffffffffffffff', 'hex') : writeVint(tagData.length), tagData ]); } /** * WebP ファイルにできる SimpleBlock の パスフィルタ */ export function WebPBlockFilter(elms: EBML.EBMLElementDetail[]): (EBML.BinaryElement & EBML.ElementDetail & {data: Buffer})[] { return elms.reduce<(EBML.BinaryElement & EBML.ElementDetail & {data: Buffer})[]>((lst, elm)=>{ if(elm.type !== "b"){ return lst; } if(elm.name !== "SimpleBlock"){ return lst; } const o = ebmlBlock(elm.data); const hasWebP = o.frames.some((frame)=>{ // https://tools.ietf.org/html/rfc6386#section-19.1 const startcode = frame.slice(3, 6).toString("hex"); return startcode === "9d012a"; }); if(!hasWebP){ return lst; } return lst.concat(elm); }, []); } /** * @param frame - VP8 BitStream のうち startcode をもつ frame * @return - WebP ファイルの ArrayBuffer */ export function VP8BitStreamToRiffWebPBuffer(frame: Buffer): Buffer { const VP8Chunk = createRIFFChunk("VP8 ", frame); const WebPChunk = concat([ new Buffer("WEBP", "ascii"), VP8Chunk ]); return createRIFFChunk("RIFF", WebPChunk); } /** * RIFF データチャンクを作る */ export function createRIFFChunk(FourCC: string, chunk: Buffer): Buffer { const chunkSize = new Buffer(4); chunkSize.writeUInt32LE(chunk.byteLength , 0); return concat([ new Buffer(FourCC.substr(0, 4), "ascii"), chunkSize, chunk, new Buffer(chunk.byteLength % 2 === 0 ? 0 : 1) // padding ]); } /* Original Metadata m 0 EBML u 1 EBMLVersion 1 u 1 EBMLReadVersion 1 u 1 EBMLMaxIDLength 4 u 1 EBMLMaxSizeLength 8 s 1 DocType webm u 1 DocTypeVersion 4 u 1 DocTypeReadVersion 2 m 0 Segment m 1 Info segmentContentStartPos, all CueClusterPositions provided in info.cues will be relative to here and will need adjusted u 2 TimecodeScale 1000000 8 2 MuxingApp Chrome 8 2 WritingApp Chrome m 1 Tracks tracksStartPos m 2 TrackEntry u 3 TrackNumber 1 u 3 TrackUID 31790271978391090 u 3 TrackType 2 s 3 CodecID A_OPUS b 3 CodecPrivate <Buffer 19> m 3 Audio f 4 SamplingFrequency 48000 u 4 Channels 1 m 2 TrackEntry u 3 TrackNumber 2 u 3 TrackUID 24051277436254136 u 3 TrackType 1 s 3 CodecID V_VP8 m 3 Video u 4 PixelWidth 1024 u 4 PixelHeight 576 m 1 Cluster clusterStartPos u 2 Timecode 0 b 2 SimpleBlock track:2 timecode:0 keyframe:true invisible:false discardable:false lacing:1 */ /* Desired Metadata m 0 EBML u 1 EBMLVersion 1 u 1 EBMLReadVersion 1 u 1 EBMLMaxIDLength 4 u 1 EBMLMaxSizeLength 8 s 1 DocType webm u 1 DocTypeVersion 4 u 1 DocTypeReadVersion 2 m 0 Segment m 1 SeekHead -> This is SeekPosition 0, so all SeekPositions can be calculated as (bytePos - segmentContentStartPos), which is 44 in this case m 2 Seek b 3 SeekID -> Buffer([0x15, 0x49, 0xA9, 0x66]) Info u 3 SeekPosition -> infoStartPos = m 2 Seek b 3 SeekID -> Buffer([0x16, 0x54, 0xAE, 0x6B]) Tracks u 3 SeekPosition { tracksStartPos } m 2 Seek b 3 SeekID -> Buffer([0x1C, 0x53, 0xBB, 0x6B]) Cues u 3 SeekPosition { cuesStartPos } m 1 Info f 2 Duration 32480 -> overwrite, or insert if it doesn't exist u 2 TimecodeScale 1000000 8 2 MuxingApp Chrome 8 2 WritingApp Chrome m 1 Tracks m 2 TrackEntry u 3 TrackNumber 1 u 3 TrackUID 31790271978391090 u 3 TrackType 2 s 3 CodecID A_OPUS b 3 CodecPrivate <Buffer 19> m 3 Audio f 4 SamplingFrequency 48000 u 4 Channels 1 m 2 TrackEntry u 3 TrackNumber 2 u 3 TrackUID 24051277436254136 u 3 TrackType 1 s 3 CodecID V_VP8 m 3 Video u 4 PixelWidth 1024 u 4 PixelHeight 576 m 1 Cues -> cuesStartPos m 2 CuePoint u 3 CueTime 0 m 3 CueTrackPositions u 4 CueTrack 1 u 4 CueClusterPosition 3911 m 2 CuePoint u 3 CueTime 600 m 3 CueTrackPositions u 4 CueTrack 1 u 4 CueClusterPosition 3911 m 1 Cluster u 2 Timecode 0 b 2 SimpleBlock track:2 timecode:0 keyframe:true invisible:false discardable:false lacing:1 */ /** * convert the metadata from a streaming webm bytestream to a seekable file by inserting Duration, Seekhead and Cues * @param originalMetadata - orginal metadata (everything before the clusters start) from media recorder * @param duration - Duration (TimecodeScale) * @param cues - cue points for clusters */ export function makeMetadataSeekable( originalMetadata: EBML.EBMLElementDetail[], duration: number, cuesInfo: {CueTrack: number; CueClusterPosition: number; CueTime: number; }[] ) : ArrayBuffer { // extract the header, we can reuse this as-is let header = extractElement("EBML", originalMetadata); let headerSize = encodedSizeOfEbml(header); //console.error("Header size: " + headerSize); //printElementIds(header); // After the header comes the Segment open tag, which in this implementation is always 12 bytes (4 byte id, 8 byte 'unknown length') // After that the segment content starts. All SeekPositions and CueClusterPosition must be relative to segmentContentStartPos let segmentContentStartPos = headerSize + 12; //console.error("segmentContentStartPos: " + segmentContentStartPos); // find the original metadata size, and adjust it for header size and Segment start element so we can keep all positions relative to segmentContentStartPos let originalMetadataSize = originalMetadata[originalMetadata.length-1].dataEnd - segmentContentStartPos; //console.error("Original Metadata size: " + originalMetadataSize); //printElementIds(originalMetadata); // extract the segment info, remove the potentially existing Duration element, and add our own one. let info : EBML.EBMLElementBuffer[] = extractElement("Info", originalMetadata); removeElement("Duration", info); info.splice(1,0, {name: "Duration", type: "f", data: createFloatBuffer(duration, 8) }) let infoSize = encodedSizeOfEbml(info); //console.error("Info size: " + infoSize); //printElementIds(info); // extract the track info, we can re-use this as is let tracks = extractElement("Tracks", originalMetadata) let tracksSize = encodedSizeOfEbml(tracks); //console.error("Tracks size: " + tracksSize); //printElementIds(tracks); let seekHeadSize = 47; // Initial best guess, but could be slightly larger if the Cues element is huge. let seekHead : EBML.EBMLElementBuffer[] = []; let cuesSize = 5 + cuesInfo.length * 15; // very rough initial approximation, depends a lot on file size and number of CuePoints let cues : EBML.EBMLElementBuffer[] = []; let lastSizeDifference = -1; // // The size of SeekHead and Cues elements depends on how many bytes the offsets values can be encoded in. // The actual offsets in CueClusterPosition depend on the final size of the SeekHead and Cues elements // We need to iteratively converge to a stable solution. const maxIterations = 10; for (let i = 0; i < maxIterations; i++) { // SeekHead starts at 0 let infoStart = seekHeadSize; // Info comes directly after SeekHead let tracksStart = infoStart + infoSize; // Tracks comes directly after Info let cuesStart = tracksStart + tracksSize; // Cues starts directly after let newMetadataSize = cuesStart + cuesSize; // total size of metadata // This is the offset all CueClusterPositions should be adjusted by due to the metadata size changing. let sizeDifference = newMetadataSize - originalMetadataSize; // console.error(`infoStart: ${infoStart}, infoSize: ${infoSize}`); // console.error(`tracksStart: ${tracksStart}, tracksSize: ${tracksSize}`); // console.error(`cuesStart: ${cuesStart}, cuesSize: ${cuesSize}`); // console.error(`originalMetadataSize: ${originalMetadataSize}, newMetadataSize: ${newMetadataSize}, sizeDifference: ${sizeDifference}`); // create the SeekHead element seekHead = []; seekHead.push({name: "SeekHead", type: "m", isEnd: false}); seekHead.push({name: "Seek", type: "m", isEnd: false}); seekHead.push({name: "SeekID", type: "b", data: new Buffer([0x15, 0x49, 0xA9, 0x66])}); // Info seekHead.push({name: "SeekPosition", type: "u", data: createUIntBuffer(infoStart)}); seekHead.push({name: "Seek", type: "m", isEnd: true}); seekHead.push({name: "Seek", type: "m", isEnd: false}); seekHead.push({name: "SeekID", type: "b", data: new Buffer([0x16, 0x54, 0xAE, 0x6B])}); // Tracks seekHead.push({name: "SeekPosition", type: "u", data: createUIntBuffer(tracksStart)}); seekHead.push({name: "Seek", type: "m", isEnd: true}); seekHead.push({name: "Seek", type: "m", isEnd: false}); seekHead.push({name: "SeekID", type: "b", data: new Buffer([0x1C, 0x53, 0xBB, 0x6B])}); // Cues seekHead.push({name: "SeekPosition", type: "u", data: createUIntBuffer(cuesStart)}); seekHead.push({name: "Seek", type: "m", isEnd: true}); seekHead.push({name: "SeekHead", type: "m", isEnd: true}); seekHeadSize = encodedSizeOfEbml(seekHead); //console.error("SeekHead size: " + seekHeadSize); //printElementIds(seekHead); // create the Cues element cues = []; cues.push({name: "Cues", type: "m", isEnd: false}) cuesInfo.forEach(({ CueTrack, CueClusterPosition, CueTime }) => { cues.push({ name: "CuePoint", type: "m", isEnd: false }); cues.push({ name: "CueTime", type: "u", data: createUIntBuffer(CueTime) }); cues.push({ name: "CueTrackPositions", type: "m", isEnd: false }); cues.push({ name: "CueTrack", type: "u", data: createUIntBuffer(CueTrack) }); //console.error(`CueClusterPosition: ${CueClusterPosition}, Corrected to: ${CueClusterPosition - segmentContentStartPos} , offset by ${sizeDifference} to become ${(CueClusterPosition - segmentContentStartPos) + sizeDifference - segmentContentStartPos}`); // EBMLReader returns CueClusterPosition with absolute byte offsets. The Cues section expects them as offsets from the first level 1 element of the Segment, so we need to adjust it. CueClusterPosition -= segmentContentStartPos; // We also need to adjust to take into account the change in metadata size from when EBMLReader read the original metadata. CueClusterPosition += sizeDifference; cues.push({ name: "CueClusterPosition", type: "u", data: createUIntBuffer(CueClusterPosition) }); cues.push({ name: "CueTrackPositions", type: "m", isEnd: true }); cues.push({ name: "CuePoint", type: "m", isEnd: true }); }); cues.push({name: "Cues", type: "m", isEnd: true}) cuesSize = encodedSizeOfEbml(cues); //console.error("Cues size: " + cuesSize); //console.error("Cue count: " + cuesInfo.length); //printElementIds(cues); // If the new MetadataSize is not the same as the previous iteration, we need to run once more. if (lastSizeDifference !== sizeDifference){ lastSizeDifference = sizeDifference; if (i === maxIterations-1) { throw new Error("Failed to converge to a stable metadata size"); } } else { // We're done! Construct the new metadata from all individual components and return. //console.error(`Final size: ${newMetadataSize}, difference: ${sizeDifference}`); break; } } let finalMetadata: EBML.EBMLElementDetail[] = [].concat.apply([], [ header, { name: "Segment", type: "m", isEnd: false, unknownSize: true }, seekHead, info, tracks, cues ]); let result = new Encoder().encode(finalMetadata); //printElementIds(finalMetadata); //console.error(`Final metadata buffer size: ${result.byteLength}`); //console.error(`Final metadata buffer size without header and segment: ${result.byteLength-segmentContentStartPos}`); return result; } /** * print all element id names in a list * @param metadata - array of EBML elements to print * export function printElementIds(metadata: EBML.EBMLElementBuffer[]) { let result: EBML.EBMLElementBuffer[] = []; let start: number = -1; for (let i = 0; i < metadata.length; i++) { console.error("\t id: " + metadata[i].name); } } */ /** * remove all occurances of an EBML element from an array of elements * If it's a MasterElement you will also remove the content. (everything between start and end) * @param idName - name of the EBML Element to remove. * @param metadata - array of EBML elements to search */ export function removeElement(idName: string, metadata: EBML.EBMLElementBuffer[]) { let result: EBML.EBMLElementBuffer[] = []; let start: number = -1; for (let i = 0; i < metadata.length; i++) { let element = metadata[i]; if (element.name === idName) { // if it's a Master element, extract the start and end element, and everything in between if (element.type === "m") { if (!element.isEnd) { start = i; } else { // we've reached the end, extract the whole thing if (start == -1) throw new Error(`Detected ${idName} closing element before finding the start`) metadata.splice(start, i - start + 1); return; } } else { // not a Master element, so we've found what we're looking for. metadata.splice(i, 1); return; } } } } /** * extract the first occurance of an EBML tag from a flattened array of EBML data. * If it's a MasterElement you will also get the content. (everything between start and end) * @param idName - name of the EBML Element to extract. * @param metadata - array of EBML elements to search */ export function extractElement(idName: string, metadata: EBML.EBMLElementBuffer[]): EBML.EBMLElementBuffer[] { let result: EBML.EBMLElementBuffer[] = []; let start: number = -1; for (let i = 0; i < metadata.length; i++) { let element = metadata[i]; if (element.name === idName) { // if it's a Master element, extract the start and end element, and everything in between if (element.type === "m") { if (!element.isEnd) { start = i; } else { // we've reached the end, extract the whole thing if (start == -1) throw new Error(`Detected ${idName} closing element before finding the start`) result = metadata.slice(start, i + 1); break; } } else { // not a Master element, so we've found what we're looking for. result.push(metadata[i]); break; } } } return result; } /** * @deprecated * metadata に対して duration と seekhead を追加した metadata を返す * @param metadata - 変更前の webm における ファイル先頭から 最初の Cluster 要素までの 要素 * @param duration - Duration (TimecodeScale) * @param cues - cue points for clusters * @deprecated @param clusterPtrs - 変更前の webm における SeekHead に追加する Cluster 要素 への start pointer * @deprecated @param cueInfos - please use cues. */ export function putRefinedMetaData( metadata: EBML.EBMLElementDetail[], info: { duration?: number, cues?: {CueTrack: number; CueClusterPosition: number; CueTime: number; }[], clusterPtrs?: number[], cueInfos?: {CueTrack: number; CueClusterPosition: number; CueTime: number; }[], } ): ArrayBuffer { if(Array.isArray(info.cueInfos) && !Array.isArray(info.cues)){ console.warn("putRefinedMetaData: info.cueInfos property is deprecated. please use info.cues"); info.cues = info.cueInfos; } let ebml: EBML.EBMLElementDetail[] = []; let payload: EBML.EBMLElementDetail[] = []; for(let i=0; i<metadata.length; i++){ const elm = metadata[i]; if(elm.type === "m" && elm.name === "Segment"){ ebml = metadata.slice(0, i); payload = metadata.slice(i); if(elm.unknownSize){ payload.shift(); // remove segment tag break; } throw new Error("this metadata is not streaming webm file"); } } // *0 *4 *5 *36 *40 *48=segmentOffset *185=originalPayloadOffsetEnd // | | | | | | | // [EBML][size]....[Segment][size][Info][size][Duration][size]...[Cluster] // | | |^inf | | // | +segmentSiz(12)+ | // +-ebmlSize(36)--+ | +-payloadSize(137)-------------+offsetEndDiff+ // | | +-newPayloadSize(??)-------------------------+ // | | | | // [Segment][size][Info][size][Duration][size]....[size][value][Cluster] // ^ | // | *??=newPayloadOffsetEnd // inf if(!(payload[payload.length-1].dataEnd > 0)){ throw new Error("metadata dataEnd has wrong number"); } const originalPayloadOffsetEnd = payload[payload.length-1].dataEnd; // = first cluster ptr const ebmlSize = ebml[ebml.length-1].dataEnd; // = first segment ptr const refinedEBMLSize = new Encoder().encode(ebml).byteLength; const offsetDiff = refinedEBMLSize - ebmlSize; const payloadSize = originalPayloadOffsetEnd - payload[0].tagStart; const segmentSize = payload[0].tagStart - ebmlSize; const segmentOffset = payload[0].tagStart; const segmentTagBuf = new Buffer([0x18, 0x53, 0x80, 0x67]); // Segment const segmentSizeBuf = new Buffer('01ffffffffffffff', 'hex'); // Segmentの最後の位置は無数の Cluster 依存なので。 writeVint(newPayloadSize).byteLength ではなく、 infinity. const _segmentSize = segmentTagBuf.byteLength + segmentSizeBuf.byteLength; // == segmentSize let newPayloadSize = payloadSize; // We need the size to be stable between two refinements in order for our offsets to be correct // Bound the number of possible refinements so we can't go infinate if something goes wrong let i; for(i = 1; i < 20; i++) { const newPayloadOffsetEnd = ebmlSize + _segmentSize + newPayloadSize; const offsetEndDiff = newPayloadOffsetEnd - originalPayloadOffsetEnd; const sizeDiff = offsetDiff + offsetEndDiff; const refined = refineMetadata(payload, sizeDiff, info); const newNewRefinedSize = new Encoder().encode(refined).byteLength; // 一旦 seekhead を作って自身のサイズを調べる if(newNewRefinedSize === newPayloadSize) { // Size is stable return new Encoder().encode( (<EBML.EBMLElementBuffer[]>[]).concat( ebml, [{type: "m", name: "Segment", isEnd: false, unknownSize: true}], refined, ) ); } newPayloadSize = newNewRefinedSize; } throw new Error("unable to refine metadata, stable size could not be found in " + i + " iterations!"); } // Given a list of EBMLElementBuffers, returns their encoded size in bytes function encodedSizeOfEbml(refinedMetaData: EBML.EBMLElementBuffer[]): number { const encorder = new Encoder(); return refinedMetaData.reduce<ArrayBuffer[]>((lst, elm)=> lst.concat(encorder.encode([elm])), []).reduce((o, buf)=> o + buf.byteLength, 0); } function refineMetadata( mesetadata: EBML.EBMLElementDetail[], sizeDiff: number, info: { duration?: number, clusterPtrs?: number[], cues?: {CueTrack: number; CueClusterPosition: number; CueTime: number; }[] } ): EBML.EBMLElementBuffer[] { const {duration, clusterPtrs, cues} = info; let _metadata: EBML.EBMLElementBuffer[] = mesetadata.slice(0); if(typeof duration === "number"){ // duration を追加する let overwrited = false; _metadata.forEach((elm)=>{ if(elm.type === "f" && elm.name === "Duration"){ overwrited = true; elm.data = createFloatBuffer(duration, 8); } }); if(!overwrited){ insertTag(_metadata, "Info", [{name: "Duration", type: "f", data: createFloatBuffer(duration, 8) }]); } } if(Array.isArray(cues)){ insertTag(_metadata, "Cues", create_cue(cues, sizeDiff)); } let seekhead_children: EBML.EBMLElementBuffer[] = []; if(Array.isArray(clusterPtrs)){ console.warn("append cluster pointers to seekhead is deprecated. please use cues"); seekhead_children = create_seek_from_clusters(clusterPtrs, sizeDiff); } // remove seek info /* _metadata = _metadata.filter((elm)=> !( elm.name === "Seek" || elm.name === "SeekID" || elm.name === "SeekPosition") ); */ // working on progress //seekhead_children = seekhead_children.concat(create_seekhead(_metadata)); insertTag(_metadata, "SeekHead", seekhead_children, true); return _metadata; } function create_seekhead(metadata: (EBML.EBMLElementDetail|EBML.EBMLElementBuffer)[], sizeDiff: number): EBML.EBMLElementBuffer[] { const seeks: EBML.EBMLElementBuffer[] = []; ["Info", "Tracks", "Cues"].forEach((tagName)=>{ const tagStarts = metadata.filter((elm)=> elm.type === "m" && elm.name === tagName && elm.isEnd === false).map((elm)=> elm["tagStart"]); const tagStart = tagStarts[0]; if(typeof tagStart !== "number"){ return; } seeks.push({name: "Seek", type: "m", isEnd: false}); switch(tagName){ case "Info": seeks.push({name: "SeekID", type: "b", data: new Buffer([0x15, 0x49, 0xA9, 0x66]) }); break; case "Tracks": seeks.push({name: "SeekID", type: "b", data: new Buffer([0x16, 0x54, 0xAE, 0x6B]) }); break; case "Cues": seeks.push({name: "SeekID", type: "b", data: new Buffer([0x1C, 0x53, 0xBB, 0x6B]) }); break; } seeks.push({name: "SeekPosition", type: "u", data: createUIntBuffer(tagStart + sizeDiff)}); seeks.push({name: "Seek", type: "m", isEnd: true}); }); return seeks; } function create_seek_from_clusters(clusterPtrs: number[], sizeDiff: number): EBML.EBMLElementBuffer[] { const seeks: EBML.EBMLElementBuffer[] = []; clusterPtrs.forEach((start)=>{ seeks.push({name: "Seek", type: "m", isEnd: false}); // [0x1F, 0x43, 0xB6, 0x75] で Cluster 意 seeks.push({name: "SeekID", type: "b", data: new Buffer([0x1F, 0x43, 0xB6, 0x75]) }); seeks.push({name: "SeekPosition", type: "u", data: createUIntBuffer(start + sizeDiff)}); seeks.push({name: "Seek", type: "m", isEnd: true}); }); return seeks; } function create_cue(cueInfos: {CueTrack: number; CueClusterPosition: number; CueTime: number; }[], sizeDiff: number): EBML.EBMLElementBuffer[] { const cues: EBML.EBMLElementBuffer[] = []; cueInfos.forEach(({CueTrack, CueClusterPosition, CueTime})=>{ cues.push({name: "CuePoint", type: "m", isEnd: false}); cues.push({name: "CueTime", type: "u", data: createUIntBuffer(CueTime) }); cues.push({name: "CueTrackPositions", type: "m", isEnd: false}); cues.push({name: "CueTrack", type: "u", data: createUIntBuffer(CueTrack) }); // video track cues.push({name: "CueClusterPosition", type: "u", data: createUIntBuffer(CueClusterPosition + sizeDiff) }); cues.push({name: "CueTrackPositions", type: "m", isEnd: true}); cues.push({name: "CuePoint", type: "m", isEnd: true}); }); return cues; } function insertTag(_metadata: EBML.EBMLElementBuffer[], tagName: string, children: EBML.EBMLElementBuffer[], insertHead: boolean = false): void { // find the tagname from _metadata let idx = -1; for(let i=0; i<_metadata.length; i++){ const elm = _metadata[i]; if(elm.type === "m" && elm.name === tagName && elm.isEnd === false){ idx = i; break; } } if(idx >= 0){ // insert [<CuePoint />] to <Cues /> Array.prototype.splice.apply(_metadata, [<any>idx+1, 0].concat(children)); }else if(insertHead){ (<EBML.EBMLElementBuffer[]>[]).concat( [{name: tagName, type: "m", isEnd: false}], children, [{name: tagName, type: "m", isEnd: true}], ).reverse().forEach((elm)=>{ _metadata.unshift(elm); }); }else{ // metadata 末尾に <Cues /> を追加 // insert <Cues /> _metadata.push({name: tagName, type: "m", isEnd: false}); children.forEach((elm)=>{ _metadata.push(elm); }); _metadata.push({name: tagName, type: "m", isEnd: true}); } } export function concat(list: Buffer[]): Buffer { return Buffer.concat(list); } export function encodeValueToBuffer(elm: EBML.MasterElement): EBML.MasterElement; export function encodeValueToBuffer(elm: EBML.ChildElementsValue): EBML.ChildElementBuffer; export function encodeValueToBuffer(elm: EBML.EBMLElementValue): EBML.EBMLElementBuffer { let data = new Buffer(0); if(elm.type === "m"){ return elm; } switch(elm.type){ case "u": data = createUIntBuffer(elm.value); break; case "i": data = createIntBuffer(elm.value); break; case "f": data = createFloatBuffer(elm.value); break; case "s": data = new Buffer(elm.value, 'ascii'); break; case "8": data = new Buffer(elm.value, 'utf8'); break; case "b": data = elm.value; break; case "d": data = new Int64BE(elm.value.getTime().toString()).toBuffer(); break; } return Object.assign({}, elm, {data}); } export function createUIntBuffer(value: number): Buffer { // Big-endian, any size from 1 to 8 // but js number is float64, so max 6 bit octets let bytes: 1|2|3|4|5|6 = 1; for(; value >= Math.pow(2, 8*bytes); bytes++){} if(bytes >= 7){ console.warn("7bit or more bigger uint not supported."); return new Uint64BE(value).toBuffer(); } const data = new Buffer(bytes); data.writeUIntBE(value, 0, bytes); return data; } export function createIntBuffer(value: number): Buffer { // Big-endian, any size from 1 to 8 octets // but js number is float64, so max 6 bit let bytes: 1|2|3|4|5|6 = 1; for(; value >= Math.pow(2, 8*bytes); bytes++){} if(bytes >= 7){ console.warn("7bit or more bigger uint not supported."); return new Int64BE(value).toBuffer(); } const data = new Buffer(bytes); data.writeIntBE(value, 0, bytes); return data; } export function createFloatBuffer(value: number, bytes: 4|8 = 8): Buffer { // Big-endian, defined for 4 and 8 octets (32, 64 bits) // js number is float64 so 8 bytes. if(bytes === 8){ // 64bit const data = new Buffer(8); data.writeDoubleBE(value, 0); return data; }else if(bytes === 4){ // 32bit const data = new Buffer(4); data.writeFloatBE(value, 0); return data; }else{ throw new Error("float type bits must 4bytes or 8bytes"); } } export function convertEBMLDateToJSDate(int64str: number | string | Date): Date { if(int64str instanceof Date){ return int64str; } return new Date(new Date("2001-01-01T00:00:00.000Z").getTime() + (Number(int64str)/1000/1000)); }