如何用ts-ebml为实时视频Blob块生成可跳转时间戳?
问题分析与解决方案
你的核心问题在于每个视频片段都被单独注入了完整的EBML元数据,导致后端拼接后的文件存在多个重复的元数据头,播放器只能识别第一个头对应的内容(前3秒),后续内容因结构错误无法正常解析,进而出现卡顿和无法跳转的情况。
直播流的片段是连续的,只需要在第一个片段中注入可seek的元数据,后续片段仅发送纯媒体内容即可。以下是具体修改方案:
1. 重构TsEBMLEngine类,维护跨片段状态
将解码器和阅读器改为实例属性,跟踪是否为第一个片段,避免重复生成元数据:
import videojs from "video.js/dist/video"; import { Buffer } from "buffer"; window.Buffer = Buffer; import { Decoder, tools, Reader } from "ts-ebml"; class TsEBMLEngine { constructor() { this.chunkDecoder = new Decoder(); this.chunkReader = new Reader(); this.chunkReader.logging = false; this.chunkReader.drop_default_duration = false; this.isFirstChunk = true; this.metadataSize = 0; } convertStream = (data) => { return data.arrayBuffer().then((buffer) => { // 解码当前片段的EBML元素并累积到阅读器 const elms = this.chunkDecoder.decode(buffer); elms.forEach((elm) => { this.chunkReader.read(elm); }); if (this.isFirstChunk) { // 第一个片段:生成可seek的元数据,返回带修正头的完整Blob this.chunkReader.stop(); const refinedMetadataBuf = tools.makeMetadataSeekable( this.chunkReader.metadatas, this.chunkReader.duration, this.chunkReader.cues ); this.metadataSize = this.chunkReader.metadataSize; const body = buffer.slice(this.metadataSize); const convertedData = new Blob([refinedMetadataBuf, body], { type: data.type }); this.isFirstChunk = false; return convertedData; } else { // 后续片段:仅返回媒体内容(跳过重复的元数据部分) const body = buffer.slice(this.metadataSize); return new Blob([body], { type: data.type }); } }); } // 重置状态,用于新的录制会话 reset() { this.chunkDecoder = new Decoder(); this.chunkReader = new Reader(); this.chunkReader.logging = false; this.chunkReader.drop_default_duration = false; this.isFirstChunk = true; this.metadataSize = 0; } } videojs.TsEBMLEngine = TsEBMLEngine; export default TsEBMLEngine;
2. 修改timestampHandler与录制流程
确保在录制开始时重置引擎状态,并正确处理每个片段:
// 组件初始化时实例化引擎 this.converter = new TsEBMLEngine(); // 录制开始事件中重置状态(比如监听videojs-record的recordStart事件) handleRecordStart() { this.converter.reset(); } timestampHandler(player) { const { length: recordedDataLength } = player.recordedData; if (recordedDataLength === 0) return; const latestChunk = player.recordedData[recordedDataLength - 1]; this.converter.convertStream(latestChunk).then((blob) => { blob.arrayBuffer().then(async (buffer) => { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } await this.$backend.videoDataSendToServer({ bytes: window.btoa(binary), id: this.videoId }); }).catch(error => { console.error('Blob转换失败:', error); }); }).catch(error => { console.error('流处理失败:', error); }); }
关键注意事项
- 后端写入逻辑:必须确保后端是追加写入到同一个文件,而非覆盖或创建新文件。
- 元数据截断验证:如果
videojs-record返回的后续片段仍包含重复元数据,需确认metadataSize的截取长度是否准确。 - 录制收尾优化:直播结束后,可对最终文件做一次元数据修正(更新总时长和cue点),提升回放体验(非实时播放场景可选)。
内容的提问来源于stack exchange,提问作者Joseph Bermudez
相关产品推荐
相关产品推荐

