Audio/Video与流媒体播放

video 与 audio

分别仅支持video/mp4video/webmvideo/oggaudio/mp3audio/wavaudio/ogg格式文件,其他格式需通过插件支持

常用属性
  • src 资源链接,或使用source标签
    如有多个source标签,则依次尝试,取第一个有效的为准
  • controls 显示控制面板
  • autoplay 自动播放(大部分浏览器无效,见下文)
  • loop 自动循环
  • muted 静音
  • preload (autoplay时失效,必然加载)
    • auto(自动加载)
    • meta(只加载元数据)
    • none(不加载)
  • poster 播放前显示的图像,仅对video有效
  • height 仅对video有效
  • width 仅对video有效
<audio controls>
  <source src="/i/horse.ogg" type="audio/ogg">
  <source src="/i/horse.mp3" type="audio/mpeg">
  Your browser does not support the audio element.
</audio>
事件顺序

当音频/视频处于加载过程中时,会依次发生以下事件:

  • loadstart 可用于判断当前浏览器是否支持自动load和自动播放,如不支持,则需要考虑hack方法
  • durationchange 时长数据发生变化
  • loadedmetadata 载入元数据 包括宽高和时长,此时如果元素宽高未指定,则会自适应宽高
  • loadeddata 当前帧的数据已加载,但没有足够的数据来播放指定音频/视频的下一帧时
  • progress 正在下载
  • canplay 部分可播放,此时播放按钮变为可点击状态
  • canplaythrough 已经全部加载完毕,可以完整播放

自动播放(包括autoplay属性和.play方法)

对已经播放的媒体再次执行load,会导致重新播放。
为兼容不同浏览器,建议总是对未自动播放做处理:

var promise = document.querySelector('video').play();

if (promise !== undefined) {
  promise.then(_ => {
    // 开始播放
  }).catch(error => {
    // 引导用户进行页面交互
  });
}
  • 普通Web端
    默认禁止自动播放(包括video/radio/Web Audio API),仅以下情况支持:
    • 媒体处于muted状态
      注意:如一开始因处于muted而自动播放,之后通过代码解除muted,则播放会中止
    • 用户进行了点击等界面交互后
      注意:在vue等spa中,路由切换前后本质上是同一个页面,因此之前路由做过的用户交互也算
    • (Chrome)用户之前在本站播放过有声音的视频,且媒体参与指数达标
  • iframe允许自动播放。
<iframe src="./bg.mp3" allow="autoplay"/>
  • 手机微信端
document.addEventListener("WeixinJSBridgeReady", function () {//不需要wx.config
  document.getElementById('bgAudio').play();
}, false);
  • APP内嵌webview
    通过对webview容器的属性设置,允许自动播放
  • Web Audio API
    加载现有的音频文件,或创建新的音频
获取视频文件宽高信息
<input type="file" id="fileButton">

fileButton.addEventListener('change', evt => {
  const file = fileButton.files[0]
  const url = URL.createObjectURL(file)
  const video = document.createElement('video')
  video.onloadedmetadata = evt => {
    URL.revokeObjectURL(url)//释放
    console.log(video.videoWidth, video.videoHeight)
  }
  video.src = url
  video.load() // fetches metadata
})

加密(防下载)
  • createObjectURL
    资源URL会变成blob:开头的虚拟地址,隐藏真实地址
var video = document.getElementById("video");
var xhr = new XMLHttpRequest();
xhr.open("GET", "./test.mp4", true);
xhr.responseType = "blob";
xhr.onload = function () {
    if (this.status == 200) {
        var blob = this.response;
        video.onload = function (e) { window.URL.revokeObjectURL(video.src); };
        video.src = window.URL.createObjectURL(blob);
    }
}
xhr.send();
  • controlslist="nodownload" 并覆盖 contextmenu
    让视频没有默认的下载按钮,并隐藏右键另存为

canvas

通过在画布中逐帧渲染视频内容,也可以播放视频

render()
function render() {
    window.requestAnimationFrame(render)
    ctx.clearRect(0, 0,canvas.width,canvas.height)
    ctx.drawImage(video, 0, 0,width,height)  //绘制视频
}

流媒体协议

流媒体协议都属于应用层,建立在TCP/IP协议之上

  • RTSP(Real Time Streaming Protocol,实时流协议)
    通常包含两个传输层协议:RTP(Real-time Transport Protocol,实时传输协议)和 RTCP(Real-time Transport Control Protocol,实时传输控制协议),结合使用:
    RTP使用偶数端口号,用于传输数据(默认基于UDP协议)。
    RTCP使用与RTP相邻的下一位奇数端口号,监控并管理数据传输
    RTSP允许双向实时数据传输,如客户端向服务器端发送请求,实现回放、快进等操作,并基于RTP和RTCP实现视频流本身的传输
    浏览器不支持该协议,不建议在web端使用
    延迟极低(UDP,1秒以内)

  • RTMP(Real Time Messaging Protocol,实时消息传输协议)
    基于TCP长连接,是Adobe为Flash播放器开发的传输协议,传输需要Flash控件支持
    传输flv(Flash Video)或f4v媒体流,播放需要Flash控件支持
    浏览器已不支持RTMP传输(因2021年后主流浏览器弃用Flash),通常在流媒体服务器将 RTMP 转为成 HTTP-FLV,再推给web端
    延迟中等(通常有2~5秒延迟)

  • HTTP-FLV
    将音视频数据封装成 FLV,然后通过 HTTP 长连接传输
    兼容性极佳(本质上就是HTTP协议),主流浏览器均支持该传输
    延迟中等(通常有2~5秒延迟)

  • HLS(HTTP Live Streaming,HTTP实时流)
    基于HTTP协议,由Apple提出
    通过m3u8文件下载多个ts文件依次播放
    兼容性极佳,主流浏览器均支持该传输
    通常会下载完数个ts文件(客户端表现为缓冲)再开始播放,以保证播放流畅性,因此延迟较高(5秒以上。假设m3u8包含5个TS文件,每个时长5秒,则延迟为25秒),不适合做直播和监控

  • MPEG-DASH(Dynamic Adaptive Streaming over HTTP)
    类似HLS,把流切分为很小的片段。
    直接建立在MSE上,比HLS兼容性更好。
    同理,延迟较高,不适合做直播和监控

  • WebSocket
    兼容性极佳,主流浏览器均支持该传输
    延迟取决于具体的传输和解析方式:

    • 小项目,可用WebSocket不断获取图片,渲染到 <img> 标签即可
    • 大项目,可传输流媒体配合 MSEJSMpeg 等使用
  • WebRTC(Web Real-Time Communication,网页实时通信)
    WebRTC 是 基于RTP/RTCP 的一整套 API,包含了流媒体协议在内的众多功能
    支持捕获客户端本地的媒体流进行P2P传输(点对点,无服务器接入)
    兼容性一般,IE11等老版本浏览器不支持。
    延迟极低(1秒以内,因基于RTP),适用于视频会议等实时通讯场景,但P2P场景下因带宽限制,不建议过多(超过4人)用户同时推流

    • 捕获客户端音视频,对应接口 MediaStream
      最初是navigator.getUserMedia,最新Web标准变更为navigator.mediaDevices.getUserMedia
    • 音视频传输,对应接口 RTCPeerConnection
    • 任意数据传输,对应接口 RTCDataChannel
    • 前端轻量插件:Camera.js
    • 框架:PeerJSwebRTC.ioSimpleWebRTCeasyrtc

原生实现摄像头调用与切换:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>WebRTC 摄像头调用与切换</title>
</head>

<body>
  <div>
    <button onclick="startVideo()">startVideo</button>
    <button onclick="closeMedia()">closeMedia</button>
    <button onclick="captureImage()">captureImage</button>
  </div>
  <br>
  <div id="deviceList"></div>
  <br>
  <video id="video" src=""></video>
  <br>
  <canvas id="canvas" width="640" height="480"></canvas>


</body>
<script>
  // 兼容处理实现通用的 navigator.mediaDevices.getUserMedia
  (function generateGetUserMedia() {

    if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};

    // 为没有 navigator.mediaDevices.getUserMedia 的环境手动实现该方法
    if (navigator.mediaDevices.getUserMedia === undefined) {

      console.log("尝试封装 navigator.mediaDevices.getUserMedia")

      navigator.mediaDevices.getUserMedia = (constraints) => {
        // 通过不同浏览器可能的位置获取 getUserMedia
        const getUserMedia =
          navigator.getUserMedia || //旧版Chrome
          (navigator.getUserMedia =
            navigator.mozGetUserMedia || //Firefox
            navigator.webkitGetUserMedia || //webkit内核
            navigator.msGetUserMedia);

        // 一些浏览器根本没实现它,则返回promise reject来保持接口一致性
        if (!getUserMedia) {
          return Promise.reject(new Error("该浏览器暂不支持访问摄像头!"));
        }
        // 否则,为老的navigator.getUserMedia方法包裹一个Promise以符合新版的标准
        return new Promise((resolve, reject) => {
          getUserMedia.call(navigator, constraints, resolve, reject);
        });
      };
    }
  })();

  // 获取指定类型的设备列表
  (function getDeviceList(kind = "videoinput") {
    return navigator.mediaDevices.enumerateDevices()
      .then(deviceInfos => {
        // console.log("deviceInfos", deviceInfos)
        //筛选摄像头
        return deviceInfos.filter(deviceItem => deviceItem.kind == kind); //audioinput audiooutput videoinput 音频输入输出、视频输入
      }).catch(error => console.error("enumerateDevices", error));
  })()
  .then(deviceList => {
    deviceList.forEach(deviceItem => {
      let deviceButton = document.createElement("button")
      deviceButton.innerHTML = deviceItem.label;
      deviceButton.style.marginRight = "10px";
      deviceButton.addEventListener("click", () => {
        startVideo(deviceItem.deviceId);
      })
      document.getElementById("deviceList").appendChild(deviceButton)
    })


  });


  let myMediaStream;
  // 播放视频
  function startVideo(deviceId) {
    const constraints = {
      audio: false, //禁用录音。
      // video: true, //启用摄像头。
      video: {
        width: 1920, //等价于 width: { ideal: 1920 }
        height: {
          ideal: 1080,
          min: 1, //不大于min的媒体来源会被舍弃
          max: 100 //大于max的媒体来源不会被舍弃,只会自动裁剪
        },
        facingMode: "user", // 等价于 facingMode: { ideal: "user" } 优先使用前置摄像头(如果有的话)
        // facingMode: { exact: "environment" } // 强制使用后置摄像头
      },
    };

    // 强制指定摄像头id
    if (deviceId) {
      constraints.video.deviceId = {
        exact: deviceId
      };
    }

    console.log("startVideo", constraints)

    window.navigator.mediaDevices
      .getUserMedia(constraints)
      .then((stream) => { // 该stream为一个MediaStream对象
        myMediaStream = stream;
        const video = document.getElementById("video");

        if ("srcObject" in video) { // 新版Chrome做法
          video.srcObject = stream;
        } else { // 旧浏览器做法
          //兼容webkit内核
          var compatibleURL = window.URL || window.webkitURL;
          video.src = compatibleURL.createObjectURL(stream);
        }
        video.onloadedmetadata = (e) => {
          video.play();
        };
      })
      //用户拒绝了许可,或者浏览器无法找到指定的媒体类型/无法满足对应的参数要求
      .catch((err) => {
        console.log("getUserMedia 失败", err.name + ": " + err.message);
      })
    // 注意,由于用户不会被要求必须作出允许或者拒绝的选择,所以返回的Promise对象可能既不会触发resolve也不会触发 reject
  }

  // 关闭摄像头/麦克风
  function closeMedia() {
    if (myMediaStream) {
      // tracks数组是按getUserMedia入参对象的倒序排列的,例如{video: true,audio: true}则tracks[0]为audio
      myMediaStream.getTracks().forEach(track => {
        track.stop();
      });
    }
  }

  // 截取画面
  function captureImage() {
    var cxt = canvas.getContext('2d');
    cxt.drawImage(document.getElementById('video'), 0, 0, 640, 480);
  }
</script>

</html>

通过 Vudio.js 实现麦克风音频波形拾取:

var canvas = document.querySelector('#canvas')

navigator.mediaDevices.getUserMedia({
 audio: true
}).then((stream) => {

  // 调用Vudio
  var vudio = new Vudio(stream, canvas, {
    accuracy: 256,
    width: 1024,
    height: 200,
    waveform: {
      fadeSide: false,
      maxHeight: 200,
      verticalAlign: 'middle',
      horizontalAlign: 'center',
      color: '#2980b9'
    }
  })

  vudio.dance()

}).catch((error) => {
 console.error(error.name || error)
})
m3u

M3U文件是一个记录索引的纯文本文件(如使用UTF-8编码,即为M3U8),记录了本地或云端的文件URL列表。

常见的在线视频网站,通过ffmpeg将完整视频切割成多个.ts文件(Transport Stream,该编码方式的文件可独立解码、无缝拼接),并生成m3u8索引文件。

  • 对于点播,客户端读取m3u8后,按顺序下载资源,依次播放。
  • 对于直播,客户端需要不断下载新的m3u8,获取新的资源并播放。

切片时,也可以生成多种清晰度的版本(同样在m3u8中记录),播放时可根据用户选择或网络状况,切换到其他清晰度的资源进行播放。

一次切分后,视频资源通过普通HTTP协议传输即可,相比于 RTSP 协议对信号源服务器压力更小。

m3u8工作原理
  • m3u8文件规范
    每一行是一个 URI,或空行,或以 # 开头的字符串,行内无空白字符。
    #EXT 开头的为标签,其他 # 开头的是注释

  • 如何观看m3u8指向的视频


ffmpeg

跨平台多媒体处理工具,可用于视频的采集、切割(截图)、合成、转码、加水印
windows build版下载下载ffmpeg-n5.0-latest-win64-gpl-5.0.zip

切割为ts文件

ffmpeg.exe -i ../../test.mp4 -hls_time 10 -hls_list_size 0 -f hls new.m3u8
生成多个.ts文件和一个new.m3u8文件,每个ts文件长10秒左右

  • -hls_time
    每片秒数,默认为2
    实际间隔和视频本身GOP(两个关键帧之间的时间间隔)有关,可配合-force_key_frames使用
  • -force_key_frames "expr:gte(t,n_forced*2)"
    强制每2秒产生一个关键帧
  • -hls_list_size
    m3u8中保存的条数,默认为5(只保留最后生成的5条)
    为0时保存所有
  • -f hls
    保存为流媒体
转换格式
ffmpeg.exe -i ../../test.mp4 ../../test.avi

web端解码播放

MSE(Media Source Extensions,媒体源扩展)

MSE API 为浏览器内置解码, 扩展了H5中的媒体相关能力
兼容性极佳(IE11也已支持大部分api),web接收到上述流媒体传输后,通过MSE API转化为MP4格式(片段则转为fMP4),提供给video标签播放

  • MediaSource 媒体资源容器
  • SourceBuffer 通过 MediaSource 提供给 video 的媒体片段
  • URL.createObjectURL() 创建一个指向一个 MediaSource 对象的 URL
jsmpeg

jsmpeg 为 JS软解码,兼容性极佳
仅支持 MPEG1 Video & MP2 Audio 格式(可通过ffmpeg转格式)
采用canvas而非video标签播放,解决了UI样式一致性、国产浏览器劫持video标签等问题

  • 通过 ajax 加载点播视频
  • 通过 Web Socket 加载直播视频流
Broadway(停更,不建议使用)

Broadway 为 JS软解码,仅支持H.264格式

基于以上解码方式的常用插件:
  • HLS.js,轻量,无UI,专用于HLS(即.m3u8文件)
  • videojs,自带video的UI封装,支持HLS(集成HLS.js),并借助Flash插件支持RTMP
  • jsmpeg ,自带canvas的UI封装,仅支持MPEG1格式
  • Streamedian,专用于RTSP
  • flv.js,b站开源插件,可在无Flash支持下播放 flv
  • tcplayerlite ,腾讯播放器
  • cyberplayer ,百度播放器
  • DPlayer,弹幕视频播放器
videojs示例:
  * 引入js和css后,对所有video标签生效
  * 直接在html的video标签中加入`data-setup`属性配置属性
  * 通过`videojs()`方法传入video元素或其id,初始化并配置属性

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>videojs</title>
  <link href="https://vjs.zencdn.net/7.10.2/video-js.css" rel="stylesheet" />
</head>

<body>
  <video id="my-video" class="video-js" data-setup='{"height":"300","width":"300"}'>
    <!-- <source src="test.mp4" type="video/mp4" /> -->
    <p class="vjs-no-js">
      please enable JavaScript
    </p>
  </video>
  <div id="btn" onclick="clickMe()">clickme</div>
</body>
<script src="https://vjs.zencdn.net/7.10.2/video.min.js"></script>

<script>


  videojs.options.autoplay = true;//全局配置
  var myPlayer = videojs('my-video', {
    //video原有基础属性/标签 
    muted: true,
    controls: true,
    loop: false,
    autoplay: false,
    sources: [{
      src: "test.mp4", type: "video/mp4"
    }],
    //videojs新增属性
    // aspectRatio:"10:1",//长宽比(优先级高于宽高属性)
    playbackRates: [0.5, 1, 1.5, 3],//可选播放速度
    // fluid:true,
  },
    function onPlayerReady() {
      console.log("muted", this.muted());
      this.muted(false);

      this.volume(0.5);//调整音量 0~1
      this.play();
      videojs.log("播放开始了1!");
    });

  //改变播放内容
  myPlayer.src({ type: 'video/mp4', src: 'http://www.example.com/path/to/video.mp4' });


  videojs.getPlayer(document.getElementById('my-video')).on("ended", function () {
    videojs.log("播放结束了1!");
  });
  videojs.getPlayer('my-video').on("ended", function () {
    videojs.log("播放结束了2!");
    this.dispose();//销毁该实例,解除所有绑定事件并移除对应video元素
    console.log("是否已销毁", this.isDisposed());
  });
  videojs.getPlayers()['my-video'].ready(function () {
    videojs.log("播放开始了2!");
    console.log("isFullscreen", myPlayer.isFullscreen());
  });


  function clickMe() {//requestFullscreen需要user gesture之后才能触发
    console.log("isFullscreen", myPlayer.isFullscreen());
    // document.documentElement.requestFullscreen();
    myPlayer.requestFullscreen();

    setTimeout(() => {//3秒后退出全屏
      document.exitFullscreen();
    }, 3000);
  };

</script>

</html>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容