我正在使用 MSE 开发视频流网站。
每个视频都转换为 FragmentedMP4 (h264,aac =>
avc1,mp4a
)
它工作得很好,但如果我想使用
webm
格式怎么办?就像 YouTube 或 Facebook,他们有时会使用它。
我想知道如何从
VP8
、VP9
或 vorbis
编解码器获取索引(如 fmp4 中的 sidx 原子)
我使用 bento4 和 ffmpeg 从视频和音频获取元数据
但bento4仅适用于MP4,并使用MP4BoxJS通过JavaScript在浏览器中解析索引。
我应该使用什么?
ffmpeg
或创建碎片 webm
或类似的东西,并获取索引流信息以将段附加到 MSE SourceBuffer
并确保它应该是可查找流..
遗憾的是没有答案,但如果有一天有人需要 JavaScript 中的 WEBM 解析器(浏览器或 NodeJS)
注意1:建议将init+index(不是从index.start到index.end)传递给函数(ArrayBuffer)(在YouTube中
Range: bytes=${initRange.start}-${indexRange.end}
(Head〜Cues)
注2:MP4/Sidx 也有
parseMP4Segments
这些功能在 YouTube(WEBM 和 MP4)视频上进行了测试,运行良好
文档:https://www.matroska.org/technical/elements.html
const EBML = {
HEAD: 0x1a45dfa3,
Segment: 0x18538067,
SeekHead: 0x114D9B74,
SegmentInformation: 0x1549A966,
Tracks: 0x1654AE6B,
Cues: 0x1C53BB6B,
CuePoint: 0xBB,
CueTime: 0xB3,
CueTrackPositions: 0xB7,
CueTrack: 0xF7,
CueClusterPosition: 0xF1,
CueRelativePosition: 0xF0,
CueDuration: 0xB2
};
/**
* @param {ArrayBuffer} buffer
*/
export function parseWEBMSegments(buffer) {
const view = new DataView(buffer);
let SeekHeadPosition = 0, CuesPosition = 0;
let offset = 0;
if (view.getUint32() === EBML.HEAD) {
offset += 4;
offset = readSize(true);
if (view.getUint32(offset) === EBML.Segment) {
offset += 4;
const end = readSize(true);
for (; offset < end;) {
if (view.getUint32(offset) === EBML.SeekHead) {
SeekHeadPosition = offset;
offset += 4;
readValue(readSize(true));
} else if (view.getUint32(offset) === EBML.SegmentInformation || view.getUint32(offset) === EBML.Tracks) {
offset += 4;
readValue(readSize(true));
} else if (view.getUint32(offset) === EBML.Cues) {
CuesPosition = offset;
offset += 4;
offset = readSize(true);
}
}
}
}
offset = CuesPosition;
function readSize(returnEndOffset = false) {
const bytes = [view.getUint8(offset)];
let checker = 0b1000_0000;
let shifter = 0b0111_1111;
for (let i = 0; i < 8; i++) {
if (bytes[0] & checker) {
bytes[0] &= shifter;
break;
}
checker >>= 1;
shifter >>= 1;
bytes.push(null);
}
offset++;
for (let i = 1; i < bytes.length; i++) {
bytes[i] = view.getUint8(offset);
offset++;
}
let size = 0;
for (const byte of bytes) size = size << 8 | byte;
return returnEndOffset ? Math.min(offset + size, buffer.byteLength) : size;
}
function readValue(end) {
let value = 0;
for (; offset < end;) {
value = value << 8 | view.getUint8(offset);
offset++;
}
return value;
}
function safeSkip() {
offset++;
const size = readSize();
offset += size;
}
if (view.getUint32(offset) !== EBML.Cues) {
throw new TypeError("This is not 'Cues' Element");
}
offset += 4;
const end = readSize(true);
const cues = [];
for (; offset < end;) {
if (view.getUint8(offset) === EBML.CuePoint) {
offset++;
const end = readSize(true);
const cue = {};
for (; offset < end;) {
if (view.getUint8(offset) === EBML.CueTime) {
if ("time" in cue) {
throw new TypeError("Invalid layout, 'CueTime' found multiple times in a single 'CuePoint'");
}
offset++;
cue.time = readValue(readSize(true)) / 1000;
} else if (view.getUint8(offset) === EBML.CueTrackPositions) {
offset++;
const end = readSize(true);
if (!("positions" in cue)) cue.positions = [];
const position = {};
for (; offset < end;) {
if (view.getUint8(offset) === EBML.CueTrack) {
offset++;
position.track = readValue(readSize(true));
} else if (view.getUint8(offset) === EBML.CueClusterPosition) {
offset++;
position.clusterPosition = SeekHeadPosition + readValue(readSize(true));
} else safeSkip();
}
cue.positions.push(position);
} else safeSkip();
}
if (!("time" in cue)) {
throw new TypeError("Invalid layout, cannot find mandatory 'CueTime' Element inside 'CuePoint'");
} else if (!("positions" in cue)) {
throw new TypeError("Invalid layout, cannot find at least one mandatory 'CueTrackPositions' Element inside 'CuePoint'");
}
cues.push(cue);
} else safeSkip();
}
return cues;
}
/**
* @param {ArrayBuffer} buffer
*/
export function parseMP4Segments(buffer) {
const view = new DataView(buffer);
let offset = 0;
let sidxEndPosition = 0;
for (; offset < buffer.byteLength;) {
const size = view.getUint32(offset);
offset += 4;
const type = view.getUint32(offset);
offset += 4;
if (type === 0x73696478) {
sidxEndPosition = offset + size - 8;
break;
}
offset += size - 8;
}
const version = view.getUint8(offset);
offset += 8;
const timescale = view.getUint32(offset);
offset += 4;
let time = 0, position = 0;
if (version === 0) {
time = view.getUint32(offset);
offset += 4;
position = sidxEndPosition + view.getUint32(offset);
offset += 4;
} else {
time = Number(view.getBigUint64(offset));
offset += 8;
position = sidxEndPosition + Number(view.getBigUint64(offset));
offset += 8;
}
offset += 2;
const count = view.getUint16(offset);
offset += 2;
const segments = [];
for (let i = 0; i < count; i++) {
const size = view.getUint32(offset) & 0x7fffffff;
const duration = view.getUint32(offset + 4) / timescale;
const segment = {
position: position,
time: time
};
segments.push(segment);
position += size;
time += duration;
offset += 12;
}
return segments;
}