自从HTML5提供了video标签,在网页中播放视频已经变成一个很是简单的事,只要一个video标签,src属性设置为视频的地址就完事了。因为src指向真实的视频网络地址,在早期通常网站资源文件不怎么经过referer设置防盗链,当咱们拿到视频的地址后能够随意的下载或使用(每次放假回家,就会有亲戚找我帮忙从一些视频网站上下东西)。javascript
目前的云存储服务商大部分都支持referer防盗链。其原理就是在访问资源时,请求头会带上发起请求的页面地址,判断其不存在(表示直接访问资源地址)或不在白名单内,即为盗链。
但是从某个时间开始咱们打开调试工具去看各大视频网站的视频src会发现,它们通通变成了这样的形式。html
拿b站的一个视频来看,红框中的视频地址,这个blob是个什么东西?。java
其实这个Blob URL也不是什么新技术,国内外出来都有一阵子了,可是网上的相关的文章很少也不是很详细,今天就和你们一块儿分享学习一下。jquery
最先是数据库直接用Blob来存储二进制数据对象,这样就不用关注存储数据的格式了。在web领域,Blob对象表示一个只读原始数据的类文件对象,虽然是二进制原始数据可是相似文件的对象,所以能够像操做文件对象同样操做Blob对象。ios
ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。咱们能够经过new ArrayBuffer(length)来得到一片连续的内存空间,它不能直接读写,但可根据须要将其传递到TypedArray视图或 DataView 对象来解释原始缓冲区。实际上视图只是给你提供了一个某种类型的读写接口,让你能够操做ArrayBuffer里的数据。TypedArray需指定一个数组类型来保证数组成员都是同一个数据类型,而DataView数组成员能够是不一样的数据类型。git
TypedArray视图的类型数组对象有如下几个:github
Blob与ArrayBuffer的区别是,除了原始字节之外它还提供了mime type做为元数据,Blob和ArrayBuffer之间能够进行转换。web
File对象其实继承自Blob对象,并提供了提供了name , lastModifiedDate, size ,type 等基础元数据。
建立Blob对象并转换成ArrayBuffer:ajax
//建立一个以二进制数据存储的html文件 const text = "<div>hello world</div>"; const blob = new Blob([text], { type: "text/html" }); // Blob {size: 22, type: "text/html"} //以文本读取 const textReader = new FileReader(); textReader.readAsText(blob); textReader.onload = function() { console.log(textReader.result); // <div>hello world</div> }; //以ArrayBuffer形式读取 const bufReader = new FileReader(); bufReader.readAsArrayBuffer(blob); bufReader.onload = function() { console.log(new Uint8Array(bufReader.result)); // Uint8Array(22) [60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62] };
建立一个相同数据的ArrayBuffer,并转换成Blob:数据库
//咱们直接建立一个Uint8Array并填入上面的数据 const u8Buf = new Uint8Array([60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]); const u8Blob = new Blob([u8Buf], { type: "text/html" }); // Blob {size: 22, type: "text/html"} const textReader = new FileReader(); textReader.readAsText(u8Blob); textReader.onload = function() { console.log(textReader.result); // 一样获得div>hello world</div> };
更多Blob和ArrayBuffer的相关内容能够参看下面的资料:
video标签,audio标签仍是img标签的src属性,不论是相对路径,绝对路径,或者一个网络地址,归根结底都是指向一个文件资源的地址。既然咱们知道了Blob实际上是一个能够看成文件用的二进制数据,那么只要咱们能够生成一个指向Blob的地址,是否是就能够用在这些标签的src属性上,答案确定是能够的,这里咱们要用到的就是URL.createObjectURL()。
const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl
这里的object参数是用于建立URL的File对象、Blob 对象或者 MediaSource 对象,生成的连接就是以blob:开头的一段地址,表示指向的是一个二进制数据。
其中localhost:1234是当前网页的主机名称和端口号,也就是location.host,并且这个Blob URL是能够直接访问的。须要注意的是,即便是一样的二进制数据,每调用一次URL.createObjectURL方法,就会获得一个不同的Blob URL。这个URL的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个Blob URL就失效。
经过URL.revokeObjectURL(objectURL) 能够释放 URL 对象。当你结束使用某个 URL 对象以后,应该经过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了,容许平台在合适的时机进行垃圾收集。
若是是以文件协议打开的html文件(即url为file://开头),则地址中 http://localhost:1234会变成null,并且此时这个Blob URL是没法直接访问的。
有时咱们经过input上传图片文件以前,会但愿能够预览一下图片,这个时候就能够经过前面所学到的东西实现,并且很是简单。
html
<input id="upload" type="file" /> <img id="preview" src="" alt="预览"/>
javascript
const upload = document.querySelector("#upload"); const preview = document.querySelector("#preview"); upload.onchange = function() { const file = upload.files[0]; //File对象 const src = URL.createObjectURL(file); preview.src = src; };
这样一个图片上传预览就实现了,一样这个方法也适用于上传视频的预览。
如今咱们有一个网络视频的地址,怎么能将这个视频地址变成Blob URL是形式呢,思路确定是先要拿到存储这个视频原始数据的Blob对象,可是不一样于input上传能够直接拿到File对象,咱们只有一个网络地址。
咱们知道平时请求接口咱们可使用xhr(jquery里的ajax和axios就是封装的这个)或fetch,请求一个服务端地址能够返回咱们相应的数据,那若是咱们用xhr或者fetch去请求一个图片或视频地址会返回什么呢?固然是返回图片和视频的数据,只不过要设置正确responseType才能拿到咱们想要的格式数据。
function ajax(url, cb) { const xhr = new XMLHttpRequest(); xhr.open("get", url); xhr.responseType = "blob"; // "text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象 xhr.onload = function() { cb(xhr.response); }; xhr.send(); }
注意XMLHttpRequest和Fetch API请求会有跨域问题,能够经过跨域资源共享(CORS)解决。
看到responseType能够设置blob和arraybuffer咱们应该就有谱了,请求返回一个Blob对象,或者返回ArrayBuffer对象转换成Blob对象,而后经过createObjectURL生成地址赋值给视频的src属性就能够了,这里咱们直接请求一个Blob对象。
ajax('video.mp4', function(res){ const src = URL.createObjectURL(res); video.src = src; })
用调试工具查看视频标签的src属性已经变成一个Blob URL,表面上看起来是否是和各大视频网站形式一致了,可是考虑一个问题,这种形式要等到请求彻底部视频数据才能播放,小视频还好说,要是视频资源大一点岂不爆炸,显然各大视频网站不可能这么干。
解决这个问题的方法就是流媒体,其带给咱们最直观体验就是使媒体文件能够边下边播(像我这样的90后男性最先体会到流媒体好处的应该是源于那款快子头的播放器),web端若是要使用流媒体,有多个流媒体协议能够供咱们选择。
HLS (HTTP Live Streaming), 是由 Apple 公司实现的基于 HTTP 的媒体流传输协议。HLS以ts为传输格式,m3u8为索引文件(文件中包含了所要用到的ts文件名称,时长等信息,能够用播放器播放,也能够用vscode之类的编辑器打开查看),在移动端大部分浏览器都支持,也就是说你能够用video标签直接加载一个m3u8文件播放视频或者直播,可是在pc端,除了苹果的Safari,须要引入库来支持。
用到此方案的视频网站好比优酷,能够在视频播放时经过调试查看Network里的xhr请求,会发现一个m3u8文件,和每隔一段时间请求几个ts文件。
可是除了HLS,还有Adobe的HDS,微软的MSS,方案一多就要有个标准点的东西,因而就有了MPEG DASH。
DASH(Dynamic Adaptive Streaming over HTTP) ,是一种在互联网上传送动态码率的Video Streaming技术,相似于苹果的HLS,DASH会经过media presentation description (MPD)将视频内容切片成一个很短的文件片断,每一个切片都有多个不一样的码率,DASH Client能够根据网络的状况选择一个码率进行播放,支持在不一样码率之间无缝切换。
Youtube,B站都是用的这个方案。这个方案索引文件一般是mpd文件(相似HLS的m3u8文件功能),传输格式推荐的是fmp4(Fragmented MP4),文件扩展名一般为.m4s或直接用.mp4。因此用调试查看b站视频播放时的网络请求,会发现每隔一段时间有几个m4s文件请求。
不论是HLS仍是DASH们,都有对应的库甚至是高级的播放器方便咱们使用,但咱们实际上是想要学习一点实现。其实抛开掉索引文件的解析拿到实际媒体文件的传输地址,摆在咱们面前的只有一个如何将多个视频数据合并让video标签能够无缝播放。
与之相关的一篇B站文章推荐给感兴趣的朋友: 咱们为何使用DASH
video标签src指向一个视频地址,视频播完了再将src修改成下一段的视频地址而后播放,这显然不符合咱们无缝播放的要求。其实有了咱们前面Blob URL的学习,咱们可能就会想到一个思路,用Blob URL指向一个视频二进制数据,而后不断将下一段视频的二进制数据添加拼接进去。这样就能够在不影响播放的状况下,不断的更新视频内容并播放下去,想一想是否是有点流的意思出来了。
要实现这个功能咱们要经过MediaSource来实现,MediaSource接口功能也很纯粹,做为一个媒体数据容器能够和HTMLMediaElement进行绑定。基本流程就是经过URL.createObjectURL建立容器的BLob URL,设置到video标签的src上,在播放过程当中,咱们仍然能够经过MediaSource.appendBuffer方法往容器里添加数据,达到更新视频内容的目的。
实现代码以下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <video controls></video> </body> </html> <script> // 封装获取视频原始数据请求 const get = (url, cb) => { // 兼容IE五、IE6 let xhr = window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject() xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { cb(xhr.response) } } xhr.responseType = 'arraybuffer' // 指定返回数据类型,这里若是使用blob类型会有问题,至于为何还不清楚 xhr.open('GET', url) xhr.send() } // 获取video DOM let vDOM = document.querySelector('video') let assetsURl = 'http://127.0.0.1:8888/frag_bunny.mp4' let mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) { let mediaSource = new MediaSource() vDOM.src = URL.createObjectURL(mediaSource) // sourceopen事件是在给video.src赋值以后触发 mediaSource.addEventListener('sourceopen', sourceopen) } else { console.error('Unsupported MIME type or codec: ', mimeCodec) } function sourceopen() { let sourceBuffer = this.addSourceBuffer(mimeCodec) get(assetsURl, buf => { sourceBuffer.appendBuffer(buf) // sourceended事件是在用户主动调用终止或者视频数据解析、播放错误时被触发 sourceBuffer.addEventListener('updateend', () => { mediaSource.endOfStream() vDOM.play() }) }) } </script>
当视频比较大时咱们改进一下,发起带range头的请求分片获取文件片断,而后追加到mediaSource,监听当前片断是否播放完,发起上述请求获取下一个片断,并且用户还可能点击进度条到其余播放点,这一点本文没有进行处理
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> </head> <body> <video controls></video> <script> var video = document.querySelector('video'); var assetURL = 'http://127.0.0.1:8888/frag_bunny.mp4'; // Need to be specific for Blink regarding codecs // ./mp4info frag_bunny.mp4 | grep Codec var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; var totalSegments = 5; var segmentLength = 0; var segmentDuration = 0; var bytesFetched = 0; var requestedSegments = []; for (var i = 0; i < totalSegments; ++i) requestedSegments[i] = false var mediaSource = null if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) { mediaSource = new MediaSource() //console.log(mediaSource.readyState); // closed video.src = URL.createObjectURL(mediaSource) mediaSource.addEventListener('sourceopen', sourceOpen) } else { console.error('Unsupported MIME type or codec: ', mimeCodec) } var sourceBuffer = null; function sourceOpen () { sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); getFileLength(assetURL, function (fileLength) { console.log((fileLength / 1024 / 1024).toFixed(2), 'MB'); //totalLength = fileLength; segmentLength = Math.round(fileLength / totalSegments); //console.log(totalLength, segmentLength); fetchRange(assetURL, 0, segmentLength, appendSegment); requestedSegments[0] = true; // ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发 video.addEventListener('timeupdate', checkBuffer); // 在用户能够开始播放视频/音频(audio/video)时触发 video.addEventListener('canplay', function () { segmentDuration = video.duration / totalSegments; video.play(); }); // 在用户开始移动/跳跃到新的音频/视频(audio/video)播放位置时触发 video.addEventListener('seeking', seek); }); }; // 获取文件大小 function getFileLength (url, cb) { var xhr = new XMLHttpRequest() xhr.open('head', url) xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { cb(xhr.getResponseHeader('content-length')) } } xhr.send() } // 获取文件片断 function fetchRange (url, start, end, cb) { var xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.responseType = 'arraybuffer' xhr.setRequestHeader('Range', 'bytes=' + start + '-' + end) xhr.onreadystatechange = function () { // 发送了一个带有Range头的get请求,服务器完成了它返回206,而不是200 if (xhr.readyState === 4 && xhr.status === 206) { console.log('fetched bytes: ', start, end) bytesFetched += end - start + 1 cb(xhr.response) } } xhr.send() } // 将文件二进制片断追加到mediaSource中 function appendSegment (chunk) { sourceBuffer.appendBuffer(chunk) } function checkBuffer (_) { var currentSegment = getCurrentSegment(); if (currentSegment === totalSegments && haveAllSegments()) { console.log('last segment', mediaSource.readyState); mediaSource.endOfStream(); video.removeEventListener('timeupdate', checkBuffer); } else if (shouldFetchNextSegment(currentSegment)) { requestedSegments[currentSegment] = true; console.log('time to fetch next chunk', video.currentTime); fetchRange(assetURL, bytesFetched, bytesFetched + segmentLength, appendSegment); } //console.log(video.currentTime, currentSegment, segmentDuration); }; function seek (e) { console.log(e); if (mediaSource.readyState === 'open') { sourceBuffer.abort(); console.log(mediaSource.readyState); } else { console.log('seek but not open?'); console.log(mediaSource.readyState); } }; function getCurrentSegment () { return ((video.currentTime / segmentDuration) | 0) + 1; }; function haveAllSegments () { return requestedSegments.every(function (val) { return !!val; }); }; function shouldFetchNextSegment (currentSegment) { return video.currentTime > segmentDuration * currentSegment * 0.8 && !requestedSegments[currentSegment]; }; </script> </body> </html>
效果:
这段代码修改自MDN的MediaSource词条中的示例代码。
此时咱们已经基本实现了一个简易的流媒体播放功能,若是愿意能够再加入m3u8或mpd文件的解析,设计一下UI界面,就能够实现一个流媒体播放器了。
最后提一下一个坑,不少人跑了MDN的MediaSource示例代码,可能会发现使用官方提供的视频是没问题的,可是用了本身的mp4视频就会报错,这是由于fmp4文件扩展名一般为.m4s或直接用.mp4,但倒是特殊的mp4文件。
一般咱们使用的mp4文件是嵌套结构的,客户端必需要从头加载一个 MP4 文件,才可以完整播放,不能从中间一段开始播放。而Fragmented MP4(简称fmp4),就如它的名字碎片mp4,是由一系列的片断组成,若是服务器支持 byte-range 请求,那么,这些片断能够独立的进行请求到客户端进行播放,而不须要加载整个文件。
咱们能够经过这个网站判断一个mp4文件是否为Fragmented MP4,网站地址。
咱们经过FFmpeg或Bento4的mp4fragment来将普通mp4转换为Fragmented MP4,两个工具都是命令行工具,按照各自系统下载下来对应的压缩包,解压后设置环境变量指向文件夹中的bin目录,就可使用相关命令了。
Bento4的mp4fragment,没有太多参数,命令以下:
mp4fragment video.mp4 video-fragmented.mp4
FFmpeg会须要设置一些参数,命令以下:
ffmpeg -i video.mp4 -movflags empty_moov+default_base_moof+frag_keyframe video-fragmented.mp4
Tips:网上大部分的资料中转换时是不带default_base_moof这个参数的,虽然能够转换成功,可是经测试若是不添加此参数网页中MediaSource处理视频时会报错。
视频的切割分段可使用Bento4的mp4slipt,命令以下:
mp4split video.mp4 --media-segment video-%llu.mp4 --pattern-parameters N