📺搞懂前端流媒体字幕

简介: 📺搞懂前端流媒体字幕

前言

在平时的生活中,我们遇到的流媒体播放很多都是有字幕的,字幕的出现可以大幅度的提升用户的观感。那么在Web中字幕到底是如何呈现的?今天就让我们一起来一探究竟。

onTimeUpdate

假设我们有一个这样的json,表示字幕数据:

  const jsonContent = [
    {
      start: "00:00:01.000",
      end: "00:00:02.000",
      text: "这是第一句字幕",
    },
    {
      start: "00:00:03.500",
      end: "00:00:04.000",
      text: "这是第二句字幕",
    },
    {
      start: "00:00:05.000",
      end: "00:00:06.000",
      text: "这是第三句字幕",
    },
  ];

其中start表示该条字幕的开始时间,end表示该条字幕的结束时间,text表示字幕的内容。在Web的流媒体中,有一个事件叫做onTimeUpdate,它会在媒体播放时以固定的间隔触发,我们可以监听这个事件然后来响应一些播放的变化。

<video
  id="video"
  controls
  autoplay
  crossorigin="anonymous"
  src="/test.mp4"
  width="500"
></video>

const handleTimeUpdate = () => {
  const video = document.querySelector("#video");
  video.addEventListener("timeupdate", function () {
    const currentTime = video.currentTime;
    console.log("currentTime", currentTime);
  });
};
handleTimeUpdate();

image.png

从上面的小例子中可以看到onTimeUpdate的触发机制以及结果。根据currentTime的变化,可以找到当前时间落在了哪一条字幕中,这样就可以实现字幕渲染的功能。

先来改一下dom结构,用一个div来展示字幕

<div class="video-container">
  <video
    id="video"
    controls
    autoplay
    crossorigin="anonymous"
    src="/test.mp4"
    width="500"
  ></video>
  <div class="subtitle"></div>
</div>

然后解析字幕数据,配合onTimeUpdate实现字幕的展示播放:

const findSubtitle = (currentTime, jsonContent) => {
  for (let i = 0; i < jsonContent.length; i++) {
    const subtitle = jsonContent[i];
    const startTime = parseTime(subtitle.start);
    const endTime = parseTime(subtitle.end);

    if (currentTime >= startTime && currentTime < endTime) {
      return subtitle;
    }
  }
  return null;
};

const parseTime = (timeStr) => {
  const [hours, minutes, seconds] = timeStr.split(":").map(parseFloat);
  return hours * 3600 + minutes * 60 + seconds;
};

const handleTimeUpdate = () => {
  const video = document.querySelector("#video");
  video.addEventListener("timeupdate", function () {
    const currentTime = video.currentTime;
    const res = findSubtitle(currentTime, jsonContent);
    const subTitle = res.text;
    const subTitleDom = document.querySelector(".subtitle");
    if (res) {
      subTitleDom.innerHTML = subTitle;
    } else {
      subTitleDom.innerHTML = "";
    }
  });
};
handleTimeUpdate();

image.png

这样我们就实现了字幕的功能,但这个实现方案是万无一失的吗?来思考一个场景:如果我在0.1s-0.3s中有一句字幕,0.5s-0.8s中有一句字幕,因为timeUpdate是浏览器根据一定的频率去触发的,假设出现了这样的一个情况,第一次触发在0.4s,第二次触发在0.9s,那这两个字幕是不是就丢了呢?

Track

<track> 标签是 HTML5 中用于提供轨道的元素之一。主要用于提供视频和音频的文本轨道(如字幕或者描述性文本)以及音频描述。

一般情况下,<track> 元素用于在 <video><audio> 元素中嵌入外部资源,以增强媒体内容的可访问性和可理解性。它可以用来添加字幕、章节标题、描述性注释等内容。<track> 标签的常见属性包括:

  • src:指定轨道文件的 URL
  • kind:指定轨道的类型,可以是 subtitles(字幕)、captions(标题)、descriptions(描述)、metadata(源数据)等。
  • srclang:指定轨道的语言,使用 ISO 639-1 语言代码表示。
  • label:指定轨道的标签或标题,用于在用户界面上显示。
  • default:可选属性,表示该轨道是否默认加载。如果存在多个轨道,只有一个可以设置为默认加载,通常用于选择用户首选的轨道。

track如果用来添加字幕的话,需要与一些字幕文件搭配使用,比如VTT文件,下面简单介绍一下VTT文件。

VTT(WebVTT)文件是一种用于表示视频文本轨道的格式。它是一种简单的文本文件,通常用于存储字幕、标题、描述性文本等内容,以便在 HTML5 视频和音频元素中进行显示。VTT 文件的名称通常以 .vtt 作为文件扩展名。

VTT 文件的结构相对简单,它由几个关键部分组成:

  • 文件头(Header) :文件头通常包含 WEBVTT 字样,用于指示这是一个 WebVTT 文件。
  • 时间标识(Timing) :时间标识部分用于指定每个文本块的起始时间和结束时间。时间格式通常为 HH:MM:SS.fff,其中 HH 表示小时,MM 表示分钟,SS 表示秒,fff 表示毫秒。时间标识通常以 --> 分隔起始时间和结束时间,例如:
00:00:01.000 --> 00:00:02.000
  • 文本内容(Text) :文本内容部分包含在时间标识之后,用于表示在指定时间范围内要显示的文本内容。每个文本块可以包含一行或多行文本,它们将在指定的时间段内逐行显示。

下面是一个简单的 VTT 文件示例:

WEBVTT

00:00:01.000 --> 00:00:02.000
这是第一句字幕。

00:00:03.500 --> 00:00:04.000
这是第二句字幕。

然后可以按照下面的代码使用:

  <video id="video" width="500" controls>
    <source src="/test.mp4" type="video/mp4">
    <track src="/en.vtt" kind="subtitles" srclang="en" label="English">
    <track src="/zh.vtt" kind="subtitles" srclang="zh" label="中文">
  </video>

zh.vtt的内容如下:

WEBVTT

00:00:01.000 --> 00:00:02.000
这是第一句字幕。

00:00:03.500 --> 00:00:04.000
这是第二句字幕。

en.vtt的内容如下:

WEBVTT

00:00:01.000 --> 00:00:02.000
This is the first subtitle.

00:00:03.500 --> 00:00:04.000
This is the second subtitle.

image.png

WebVTT中还有一些精细化控制的API,可以参照这个文档WebVTT。下面就简单举两个例子,其他的能力就请查阅文档,这里就不一一展开。

比如说可以通过lineposition来调整字幕的位置:

WEBVTT

00:00:01.000 --> 00:00:02.000 line:38% position:35%
这是第一句字幕。

00:00:03.500 --> 00:00:04.000 line:40% position:35%
这是第二句字幕。
  • line 属性用于设置文本的垂直位置。它表示文本相对于媒体区域高度的百分比位置。
  • position 属性用于设置文本的水平位置。它表示文本相对于媒体区域宽度的百分比位置。

image.png

再比如通过伪类来设置字幕的一些样式:

#video::cue(c.red) {
    color: red;
}
WEBVTT

00:00:01.000 --> 00:00:02.000 line:38% position:35%
<c.red>这是第一句字幕。</c.red>

00:00:03.500 --> 00:00:04.000 line:40% position:35%
这是第二句字幕。

image.png

完全自定义字幕样式

image.png

但仔细看上面的字幕,可以发现字幕有一个半透明的黑色背景,我尝试了很多方法都无法将这个背景去掉,查阅资料也是说这是由于不同浏览器对于流媒体的字幕编码导致的,我们已经完全控制了字幕的时机,但是没有完完全全控制字幕的样式。

我们可以将track标签的kind属性赋值为metadata,当字幕变更时会触发一个cueChange事件,通过这个事件拿到字幕,然后塞进某个dom中来完全控制字幕的样式。

    const fileVtt = () => {
      const video = document.querySelector("#video");
      const track = document.createElement("track");
      track.default = true;
      track.kind = "metadata";
      function convertJsonToVtt(jsonContent) {
        let vttContent = "WEBVTT\n\n";

        jsonContent.forEach((item, index) => {
          vttContent += `${index + 1}`;
          vttContent += "\n";
          vttContent += `${item.start} --> ${item.end}`;
          vttContent += "\n";
          if (item.type) {
            vttContent += `<c.mn>${item.text}</c.mn>`;
          } else {
            vttContent += `${item.text}`;
          }
          vttContent += "\n\n";
        });

        return vttContent;
      }
      const vttContent = convertJsonToVtt(jsonContent);
      // 将VTT内容保存为文件
      const blob = new Blob([vttContent], {
        type: "text/vtt;charset=utf-8",
      });
      const url = URL.createObjectURL(blob);

      track.src = url;
      video.appendChild(track);

      setTimeout(() => {
        const track = video.textTracks[0];

        track.addEventListener("cuechange", function () {
          const cues = track.activeCues;
          for (var i = 0; i < cues.length; i++) {
            const cue = cues[i];
            const div = document.createElement("div");
            div.append(cue.getCueAsHTML());
            const subTitleDom = document.querySelector(".subtitle");
            subTitleDom.innerHTML = `<div style="color:yellow">${div.innerHTML}<div>`
          }
        });
      });
    };

    fileVtt();

简单介绍一下上面的实现:

  • json格式的字幕数据转换成VTT格式
  • 监听cueChange事件获取到字幕
  • 动态插入字幕

最后

以上就是本文对流媒体字幕的一些认识,如果你觉得有意思的话,点点关注点点赞吧。欢迎评论区交流

相关文章
|
2月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
196 2
|
2月前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
56 0
|
2月前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
136 1
|
2月前
|
前端开发 算法 测试技术
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
本文对比测试了通义千文、文心一言、智谱和讯飞等多个国产大模型在处理基础计数问题上的表现,特别是通过链式推理(COT)提示的效果。结果显示,GPTo1-mini、文心一言3.5和讯飞4.0Ultra在首轮测试中表现优秀,而其他模型在COT提示后也能显著提升正确率,唯有讯飞4.0-Lite表现不佳。测试强调了COT在提升模型逻辑推理能力中的重要性,并指出免费版本中智谱GLM较为可靠。
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
|
3月前
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
4月前
|
存储 前端开发 JavaScript
前端语言串讲 | 青训营笔记
前端语言串讲 | 青训营笔记
52 0
|
6月前
|
JSON 前端开发 JavaScript
前端Ajax、Axios和Fetch的用法和区别笔记
前端Ajax、Axios和Fetch的用法和区别笔记
128 2
|
6月前
|
前端开发 JavaScript 数据库
如何实现前后端分离-----前端笔记
如何实现前后端分离-----前端笔记