分享-欢迎使用zwight个人网站系统
×

Web Codecs-媒体编解码实践

前言了解到这个API是来源于阿里云平台的开发者社区中一篇关于“Web端短视频编辑器的设计与实现”的介绍,这篇文章中介绍了如何设计一个视频编辑器,从架构到功能到实现,因此才生出通过代码去简单的实现他的这篇文章中描述的功能,然而在实现中缺少更加专业的音视频相关方向的专业知识,所以只能大概实现将一个视频解码到再编码导出的一个过程。Web Codecs API简介现代浏览器提供了很多种方式来播放或录制音视频,比如Media Stream API,Media Recoding API,Web Audio API,WebRTC API等等。然而,这些 API 不允许开发人员处理视频流的单个帧和未混合的编码音频或视频块。因此开发者通常使用 WebAssembly 来绕过此限制,并在浏览器中使用媒体编解码器。但是,这需要额外的带宽来下载浏览器中已经存在的编解码器,从而降低性能和能效,并增加额外的开发开销。WebCodecs API 提供对浏览器中已有的编解码器的访问。它可以访问原始视频帧、音频数据块、图像解码器、音频和视频编码器和解码器视频处理工作流那WebCodecs是如何工作的。 首先我们需要把视频文件解封装获得音视频块,然后通过WebCodecs的VideoDecoder和AudioDecoder解码分别获得VideoFrame(Frame是视频处理中的一个核心概念。一个Frame就是一帧,代表了视频中的一整幅画面。视频编码器将Frame转换为一个已经编码的数据块,解码器则把已经编码的数据块转化为Frame)和AudioData。 VideoFrame可以通过createImageBitmap方法将Frame转换成ImageBitmap,然后可以用于canvas的绘制——调用drawImage(),或者texImage2D()。这样的话,我们拿到解码后的Frame数据,就可以很方便的用canvas绘制出来。这个过程是很高效的。 AduioData可以通过MediaStream在前端应用中进行播放 在我们拿到了视频和音频数据后可以通过WebCodecs的VideoEncoder和AudioEncoder进行编码,然后通过Muxer工具对其进行封装导出mp4文件。整个工作的流程如下图。接下来针对这个流程详细讲解实现过程一、视频解封装 音视频在解编码之前需要对视频文件进行解封装拿到视频文件的原始编码数据,而WebCodecs并没有提供相应的解封装API,因此就需要借助MP4Box.js/mux.js/ffmpeg 之类的工具拿到原始编码数据。在这里我们使用的是MP4Box.jsfile = MP4Box.createFile(); file.onReady = (info: any) => {   videoTrack = info.videoTracks[0];   audioTrack = info.audioTracks[0];     videoW = videoTrack.track_width;   videoH = videoTrack.track_height;   // 初始化解码器   setupVideoDecoder({       codec: videoTrack.codec,       codedWidth: videoW,       codedHeight: videoH,       description: getExtradata(),   });   setupAudioDecoder({       codec: audioTrack.codec,       sampleRate: audioTrack.audio.sample_rate,       numberOfChannels: audioTrack.audio.channel_count,   });      file.start(); }) file.onSamples = (trackId: any, _ref: any, samples: string | any[]) => {   if (videoTrack.id === trackId) {                           for (const sample of samples) {           const type = sample.is_sync ? 'key' : 'delta';           const chunk = new window.EncodedVideoChunk({               type,               timestamp: sample.cts,               duration: sample.duration,               data: sample.data,           });          videoDecoder.decode(chunk);       }      if (samples.length === videoTrack.nb_samples) {           videoDecoder.flush();       }       return;   }   if (audioTrack.id === trackId) {       for (const sample of samples) {           const type = sample.is_sync ? 'key' : 'delta';           const chunk = new window.EncodedAudioChunk({               type,               timestamp: sample.cts,               duration: sample.duration,               data: sample.data,               offset: sample.offset,           });         audioDecoder.decode(chunk);       }       if (samples.length === audioTrack.nb_samples) {           audioDecoder.flush();       }   } }) fetch(url).then((response) => {     let offset = 0;     let buf: any;     const reader = response.body?.getReader();     const push = () => reader?.read().then(({ done, value }) => {         if (done === true) {             file.flush(); // 触发file.onReady             return;         }         buf = value.buffer;         buf.fileStart = offset;         offset += buf.byteLength;         file.appendBuffer(buf);         push();     }).catch((e) => {         console.error('reader error ', e);     });     push(); });首先通过MP4Box创建一个文件对象,然后通过fetch请求拿到文件的FileReader,通过读取FileReader的read方法获取文件流的buffer并将它添加到file对象中,在文件读取完成之后调用file的flush方法可以触发file的onReady,同时在onReady中会返回视频文件的轨道数据videoTracks和audioTracks,在file准备好之后执行file的start方法就是对视频文件的解封装,执行start方法后立即触发file的onSamples监听事件,这个事件一般有几条轨道就会触发几次,这里只有视频和音频各一条轨道,所以触发了两次,每次触发返回的值都是对应轨道的帧数据,之后遍历每个轨道的帧数据,分别通过EncodedAudioChunk和EncodedVideoChunk生成音视频的chunk块,之后就可以通过VideoDecoder和AudioDecoder对其进行解码获得每帧的数据二、音视频解码 在我们通过视频解封装拿到了音频和视频的chunk数据之后,我们就可以通过window的VideoDecoder和AudioDecoder两个api分别对视频图像和音频进行解码,一般我们会将初始化解码器放在file对象的onReady方法中去初始化,在file文件加载完成并且解析出chunk数据之后就可以直接对chunk数据进行解码videoDecoder.decode(chunk);videoW = videoTrack.track_width; videoH = videoTrack.track_height; setupVideoDecoder({     codec: videoTrack.codec, // 解码格式     codedWidth: videoW, // 解码数据宽度     codedHeight: videoH, // 解码数据高度     description: getExtradata(), // AVCDecoderConfigurationRecord(AVC Squence Header),包含H.264解码相关信息,如果是avc格式,需要提供,具体的解释需要去了解H.264 解码器avcC }); setupAudioDecoder({     codec: audioTrack.codec, // 解码格式     sampleRate: audioTrack.audio.sample_rate, // 音频采样率     numberOfChannels: audioTrack.audio.channel_count, // 音频通道数 }); // 视频解码 const setupVideoDecoder = (config: any) => {     output.width = outputW;     output.height = outputH;     videoDecoder = new window.VideoDecoder({         output: (videoFrame: any) => {             createImageBitmap(videoFrame).then((img) => {                 videoFrames.push(img);                 videoFrame.close(); // 因为后续只需要对img操作,所以我们使用完后需要释放掉视频帧的资源 // 在canvas中绘画第一帧                 if (videoFrames.length === videoTrack.nb_samples) {                     setTimeout(() => {                         drawVideoImage(videoFrames[0]);                     }, 150);                 }             });         },         error: (e: any) => {             console.error('VideoDecoder error : ', e);         },     });     videoDecoder.configure(config);     file.setExtractionOptions(videoTrack.id, null, { nbSamples: nbSampleMax }); // 设置解码选项,这里设置的单次解码最大帧数 }; // 音频解码 const setupAudioDecoder = (config: any) => {     audioDecoder = new window.AudioDecoder({         output: (audioFrame: any) => {             decodedAudioFrames.push(audioFrame);             if (decodedAudioFrames.length === audioTrack.nb_samples) {                 console.log(decodedAudioFrames)             }         },         error: (err: any) => {             console.error('AudioDecoder error : ', err);         },     });     audioDecoder.configure(config);     file.setExtractionOptions(audioTrack.id, null, { nbSamples: nbSampleMax }); };视频解码器初始化在传入我们需要的配置后即可完成初始化,之后在调用decode传入chunk数据在output回调方法中可以返回处理后的视频帧videoFrame,这里我们通过createImageBitmap方法将每个视频帧处理为图片数据,这样方便我们后面绘制到canvas,之后将我们解码出来的数据存入数组中,音频解码器和视频解码器方法类似,只是传入的配置数据不一样,解码chunk数据后也能获得音频数据audioFrame(AduioData)三、音视频帧数据播放如果我们是开发一个视频播放器或者是视频剪辑的话,在我们对音视频解码完成之后可以通过图像和声音的形式对解码后的数据进行预览播放const playVideo = (start: number = 0) => {     ctx?.clearRect(0, 0, outputW, outputH)     const playLoop = (index: number) => {         const imageBitmap = videoFrames[index];         if (index === (videoFrames.length - 1)) {             currentTime = 0         }         if (!imageBitmap) return;         setTimeout(() => {             drawVideoImage(imageBitmap)             if (PAUSE) {                 currentTime = videoFrameDurationInMicrosecond * index             } else {                 playLoop(index + 1)             }         }, videoFrameDurationInMicrosecond / 1000 );     }     drawVideoImage(videoFrames[start])     playLoop(start + 1) } const playAudio = async (start: number = 0) => {     if (start === 0) {         const ele = document.getElementsByClassName('audio-play-ele')?.[0];         if (ele) {             document.body.removeChild(ele)         }         const audio = document.createElement('audio');         audio.style.display = 'none';         audio.autoplay = true;         audio.className = 'audio-play-ele'         document.body.appendChild(audio);         const generator = new window.MediaStreamTrackGenerator({             kind: 'audio'         });         const {             writable         } = generator;         writer = writable.getWriter();         const mediaStream = new MediaStream([generator]);         audio.srcObject = mediaStream;     }     for (let index = start; index < decodedAudioFrames.length; index++) {         const audioFrame = decodedAudioFrames[index].clone();         const timeout = setTimeout(() => {             writer.write(audioFrame);             audioFrame.close()             clearTimeout(timeout)         }, audioFrame.timestamp / 1000 );     } }视频的播放较为简单,通过遍历循环视频帧,再加上定时器,通过canvas按顺序绘制每一帧的视频图像,在视觉上就形成了视频播放效果,音频的播放需要借助audio标签,以及系统MediaStream和MediaStreamTrackGenerator,利用循环遍历和定时器循环将音频数据不断写入到writable中,WritableStream允许将媒体帧写入MediaStreamTrackGenerator,它本身就是一个MediaStreamTrack。如果kind属性是“audio”,流会接受AudioFrame对象,不接受任何其他类型的对象。再将MediaStreamTrackGenerator初始化到MediaStream中,然后使用audio的srcObject属性播放音频流四、音视频编码编码过程可以看作是解码过程的一个反过程因为在前面我们把videoFrame转成了image bitmap,所以编码的时候我们还需要将image bitmap再转回videoFrame,这里我们使用window的VideoFrame方法进行转换const imageBitmap = videoFrames[index] const timestamp = videoFrameDurationInMicrosecond * index; const videoFrame = new window.VideoFrame(imageBitmap, { timestamp, duration: videoFrameDurationInMicrosecond });转换得到的videoFrame,经过VideoEncoder编码后,都会生成一个EncodedVideoChunk对象。首先,我们创建一个VideoEncode对象,并且进行初始化。setupVideoEncoder({     codec: 'avc1.42001E', //指定编码格式     width: outputW,     height: outputH,     hardwareAcceleration: 'prefer-software', // 硬件加速     framerate: videoFramerate, //每秒帧率     bitrate: BITRATE, //比特率     // avc: { format: 'avc' }, }); const setupVideoEncoder = (config: any) => {   videoEncoder = new window.VideoEncoder({         output: (encodedChunk: any, config: any) => { //处理编码块         },         error: (err: any) => {             console.error('VideoEncoder error : ', err);         },     });    videoEncoder.configure(config);//配置编码参数 }现在我们可以用进行编码了。通过encode()方法,可以传递数据到编码器。encode()会马上返回,并且不会立即返回处理结果。结果我们要在初始化参数output的回调中获取。可以同时调用多次encode()方法,编码器维护一个队列,按照先进先出的原则逐步处理。有些异常会在调用encode()时立即抛出,有些异常会在回调error里处理,这里要注意区分一下。如果处理成功了,我们就可以在output回调里拿到编码后的数据了音频的编码过程和视频编码过程一致,只是传入配置编码参数不一样setupAudioEncoder({     codec: 'opus', //指定编码格式,Chrome 的 WebCodecs AudioEncoder现在只支持opus格式     sampleRate: audioTrack.audio.sample_rate, // 音频采样率     numberOfChannels: audioTrack.audio.channel_count, // 通道数     bitrate: audioTrack.bitrate, // 比特率 }); const setupAudioEncoder = (config: any) => {   audioEncoder = new window.AudioEncoder({       output: (encodedChunk: any, _config: any) => {       },       error: (err: any) => {           console.error('AudioEncoder error : ', err);       },   });   audioEncoder.configure(config); };注意:目前Chrome 的 WebCodecs AudioEncoder现在只支持opus格式,并且该格式导出的音频也只能在浏览器中才能正常播放,系统的媒体播放器本身不支持opus格式五、视频导出在所有的视频帧都编码完毕后,我们就需要对编码后的数据进行封装了,本文将其封装为最常用的视频格式——mp4。在这里我是利用mp4box的封装器,首先我们需要先创建一个输出的mp4box的文件对象const outputFile = MP4Box.createFile();,之后我们对编码过程中获得的chunk数据进行封装首先是对视频帧处理:const videoEncodingTrackOptions = {     timescale: ONE_SECOND_IN_MICROSECOND, // 文件媒体在1秒时间内的刻度,可理解为1s长度的时间单元数     width: outputW,     height: outputH,     nb_samples: videoTrack.nb_samples,     media_duration: videoTrack.nb_samples * 1000 / FPS,     brands: ['isom', 'iso2', 'avc1', 'mp41'], // 兼容性的版本     avcDecoderConfigRecord: null, }; const videoEncodingSampleOptions = {     duration: videoFrameDurationInMicrosecond,     dts: 0,     cts: 0,     is_sync: false, }; if (encodingVideoTrack == null) {     videoEncodingTrackOptions.avcDecoderConfigRecord = config.decoderConfig.description;     encodingVideoTrack = outputFile.addTrack(videoEncodingTrackOptions); } const buffer = new ArrayBuffer(encodedChunk.byteLength); encodedChunk.copyTo(buffer); videoEncodingSampleOptions.dts = encodedVideoFrameCount * MICROSECONDS_PER_FRAME; videoEncodingSampleOptions.cts = encodedVideoFrameCount * MICROSECONDS_PER_FRAME; videoEncodingSampleOptions.is_sync = encodedChunk.type === 'key'; outputFile.addSample(encodingVideoTrack, buffer, videoEncodingSampleOptions); encodedVideoFrameCount++; if (encodedVideoFrameCount === videoFrames.length) {     onVideoEncodingComplete(); } else {     encodeVideo(encodedVideoFrameCount) // 遍历所有视频帧 }首先处理第一帧的时候需要在outputFile中添加一个视频轨道encodingVideoTrack,然后将encodedChunk转换为ArrayBuffer对象,最后利用mp4box的addSample方法将编码后的帧数据插入到视频轨道。音频数据的处理和视频数据的处理方式一样,也是首先添加一个音频轨道,然后将encodedChunk转换为ArrayBuffer对象,最后利用mp4box的addSample方法将编码后的帧数据插入到音频轨道。const audioEncodingTrackOptions = {     timescale: SAMPLE_RATE,     media_duration: 0,     duration: 0,     nb_samples: 0,     samplerate: SAMPLE_RATE,     width: 0,     height: 0,     hdlr: 'soun',     name: 'SoundHandler',     type: 'Opus', }; const audioEncodingSampleOptions = {     duration: 0,     dts: 0,     cts: 0,     is_sync: false, }; if (encodingAudioTrack === null) {     totalaudioEncodeCount = Math.floor(audioTotalTimestamp / encodedChunk.duration);      audioEncodingTrackOptions.nb_samples = totalaudioEncodeCount;     const trackDuration = audioTotalTimestamp / ONE_SECOND_IN_MICROSECOND;     audioEncodingTrackOptions.duration = trackDuration * SAMPLE_RATE;     audioEncodingTrackOptions.media_duration = trackDuration * SAMPLE_RATE;     encodingAudioTrack = outputFile.addTrack(audioEncodingTrackOptions); } const buffer = new ArrayBuffer(encodedChunk.byteLength); encodedChunk.copyTo(buffer); const sampleDuration = encodedChunk.duration / ONE_SECOND_IN_MICROSECOND * SAMPLE_RATE; audioEncodingSampleOptions.dts = encodedAudioFrameCount * sampleDuration; audioEncodingSampleOptions.cts = encodedAudioFrameCount * sampleDuration; audioEncodingSampleOptions.duration = sampleDuration; audioEncodingSampleOptions.is_sync = encodedChunk.type === 'key'; outputFile.addSample(encodingAudioTrack, buffer, audioEncodingSampleOptions); encodedAudioFrameCount++; if (encodedAudioFrameCount >= totalaudioEncodeCount) {     console.log(encodedAudioFrameCount)     onAudioEncodingComplete(); }完成音视频的处理之后直接调用outputFile的save方法即可保存出视频文件 outputFile.save('test.mp4');六、总结这里主要是介绍了web codecs的相关API的使用,其中有许多音视频相关的知识不做详细介绍,因为这方面的知识点比较庞大,暂时没办法对它进行详细的讲解。对于WebCodecs来说,它提供了我们访问底层编码解码接口的能力,使我们可以更深入的进行web音视频开发,是音视频开发的又一利器。但是,现在这个标准还没完全确定,有些还在调整中。而且很多API兼容性较差,目前应该是还没办法投产,不过在它的帮助下我们可以使用ffmpeg等工具用目前的解封装方式结合当前的音视频编解码去进行音视频的开发

Web Speech API语音识别和合成

前言Web Speech API 提供了两类不同方向的函数——语音识别和语音合成 (也被称为文本转为语音,英语简写是 tts)。传统的语音识别和合成一般使用各个厂商提供的开放API,或者有实力的企业可以自己研发对应的API使用,而有了Web Speech之后我们可以直接在Web App中原生使用语音识别技术,尽管目前这项技术还不太成熟,这篇文章将为大家简单的介绍一下这个API。Web Speech有两类API:1、语音识别(Speech Recognition)-----------> 语音转文字2、语音合成(Speech Synthesis) -----------> 文字变语音一、Speech Recognition(语音识别)Speech recognition(语音识别) 涉及三个过程:首先,需要设备的麦克风接收这段语音;其次,speech recognition service(语音识别服务器) 会根据一系列语法 (基本上,语法是你希望在具体的应用中能够识别出来的词汇) 来检查这段语音;最后,当一个单词或者短语被成功识别后,结果会以文本字符串的形式返回 (结果可以有多个),以及更多的行为可以设置被触发。Web Speech API 有一个主要的控制接口——SpeechRecognition, 外加一些如表示语法、表示结果等等亲密相关的接口。通常,设备都有可使用的默认语音识别系统,大部分现代操作系统使用这个语音识别系统来处理语音命令,比如 Mac OS X 上的 Dictation,iOS 上的 Siri,Win10 上的 Cortana,Android Speech 等等。1、浏览器支持对于 Web Speech API speech recognition(语音识别) 的支持,在各浏览器中还不成熟,主要浏览器兼容性如下Chrome 现在支持的是带有前缀的 speech recognition,因此在 code 开始部分得加些内容保证在需要前缀的 Chrome 和不需要前缀的像 Firefox 中,使用的 object 都是正确的const SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition if (SpeechRecognition){     const speechrecognition = new SpeechRecognition() }注意,在浏览器中调用此功能语音识别是不准确的。Chrome 的处理方式时获取音频并将其发送到 Google 的服务器以转换为文本,目前在不翻墙的情况下使用该API会报网络问题的错误。而在safair浏览器中使用该API的时候会获取设备本身的语音识别权限进行转换文本,无需翻墙2、属性和方法a)、属性continous:是否让浏览器始终进行语言识别,默认为false,也就是说,当用户停止说话时,语音识别就停止了。这种模式适合处理短输入的字段。maxAlternatives:设置返回的最大语音匹配结果数量,默认为1lang:设置语言类型,默认值就继承自HTML文档的根节点或者是祖先节点的语言设置。b)、方法start():启动语音识别stop():停止语音识别abort():中止语音识别3、事件开始识别语音时浏览器会询问用户是否许可浏览器获取麦克风数据这个API提供了11个事件。audiostart:当开始获取音频时触发,也就是用户允许时。audioend:当获取音频结束时触发error:当发生错误时触发nomatch:当找不到与语音匹配的值时触发result: 当得到与语音匹配的值时触发,它传入的是一个SpeechRecognitionEvent对象,它的results属性就是语音匹配的结果数组,最匹配的结果排在第一位。该数组的每一个成员是SpeechRecognitionResult对象,该对象的transcript属性就是实际匹配的文本,confidence属性是可信度(在0到1之间)soundstartsoundendspeechstartspeechendstart:当开始识别语言时触发end:当语音识别断开时触发4、实例<Button onClick={handleToggleSpeech}>{recordStatus === 0 ? 'Start' : 'Stop'}</Button> const initRecognition = () => {     const SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition     if (SpeechRecognition){         const speechrecognition = new SpeechRecognition()         // speechrecognition.lang = 'cmn-Hans-CN'         speechrecognition.continuous = true;         speechrecognition.interimResults = true;         setRecognition(speechrecognition)                speechrecognition.onresult = handleResult speechrecognition.onerror = (e) => {             console.error('错误,请重试', e);         }         speechrecognition.onend = (e) => {             console.log('录音结束', e);             stop()         }     } } const handleResult = (event) => {     if(event.results.length > 0) {         const arr = []         for (const res of event.results) { ①             arr.push(res[0].transcript)         }         setSpeechValue(arr.join(';'))     } } const handleToggleSpeech = () => {     recordStatus === 0 ? start() : stop() } const start = () => {      recognition.start();      setRecordStatus(1) };  const stop = () => {      setRecordStatus(0)     recognition && recognition.stop(); }①:SpeechRecognitionEvent.results属性返回的是一个SpeechRecognitionResultList对象 (这个对象会包含SpeechRecognitionResult 对象们),它有一个 getter,所以它包含的这些对象可以像一个数组被访问到。每个SpeechRecognitionResult 对象包含的 SpeechRecognitionAlternative 对象含有一个被识别的单词。这些SpeechRecognitionResult 对象也有一个 getter,所以[0] 返回的是其中包含的第一个SpeechRecognitionAlternative对象。最后返回的transcript属性就是被识别单词的字符串。二、Speech synthesis(语音合成)语音合成 (也被称作是文本转为语音,英语简写是 tts) 包括接收 app 中需要语音合成的文本,再在设备麦克风播放出来这两个过程。Web Speech API 对此有一个主要控制接口——SpeechSynthesis,外加一些处理如何表示要被合成的文本 (也被称为 utterances),用什么声音来播出 utterances 等工作的相关接口,这一步使用SpeechSynthesisUtterance来对文本处理。同样的,许多操作系统都有自己的某种语音合成系统,在这个任务中我们调用可用的 API 来使用语音合成系统。1、浏览器支持Web Speech API 语音合成部分在各浏览器中还是在发展,还不成熟,浏览器兼容性如下图:SpeechSynthesis API在Chrome和Firefox中都不需要前缀2、属性和方法1)、SpeechSynthesisUtterance对象SpeechSynthesisUtterance对象的构造可以直接传递要读的内容,这里我们也可以通过给实例对象的属性赋值来设置要读的内容,使用方式如下const utterance: SpeechSynthesisUtterance = new SpeechSynthesisUtterance(speakValue); utterance.voice = currentVoice as SpeechSynthesisVoice; utterance.lang = "zh-CN";  // 使用的语言:中文 utterance.volume = 10;      // 声音音量:1 utterance.rate = 1;        // 语速:1 utterance.pitch = 1;a)、属性text:要合成的文字内容,字符串volume:声音的音量,区间范围是0到1,默认是1rate:语速,数值,默认值是1,范围是0.1到10,表示语速的倍数,例如2表示正常语速的两倍pitch:表示说话的音高,数值,范围从0(最小)到2(最大)。默认值为1voice:说出话语的声音lang:使用的语言,字符串, 例如:“zh-CN”b)、方法onstart:语音开始合成时触发onpause:语音暂停时触发onresume:语音合成重新开始时触发onend:语音结束时触发2)、speechSynthesis对象创建完SpeechSynthesisUtterance对象之后,把这个对象传递给speechSynthesis对象的speak方法中。该对象主要是控制合成行为的,实现如下:speechSynthesis.speak(utterance);a)、方法speak:只能接收SpeechSynthesisUtterance作为唯一的参数,作用是读合成的话语stop:立即终止合成过程pause:暂停合成过程cancel:接口从话语队列中删除所有的话语resume:重新开始合成过程getVoices: 此方法用来返回浏览器支持的语音包列表数组,SpeechSynthesisUtterance对象中使用的voice就是从这个API获取到的数组中设置的,该数据较大程度取决于操作系统3、实例<Input.TextArea     onChange={handleChange}     style={{ width: '50vw', marginBottom: 16 }}     rows={5} /> <div style={{ display: 'flex' }}>     <Select value={currentVoice?.voiceURI} showSearch onChange={handleChangeVoice} style={{ width: 200, marginRight: 16 }}>         {voicesList.map(d => {             return <Select.Option key={d.voiceURI} value={d.voiceURI}>{d.name}</Select.Option>         })}     </Select>     <Button onClick={handleSpeak}>Speak</Button> </div> const [speakValue, setSpeakValue] = useState('') const [currentVoice, setCurrentVoice] = useState<SpeechSynthesisVoice>(); const [voicesList, setVoicesList] = useState<Array<SpeechSynthesisVoice>>([]) const getVoice = () => {     const synth = window.speechSynthesis;     const voices: Array<SpeechSynthesisVoice> = synth.getVoices();     const defaultVoice = voices.find(d => d.default)     setVoicesList(voices);     setCurrentVoice(defaultVoice) } const handleChange = (e: BaseSyntheticEvent) => {     setSpeakValue(e.target.value) } const handleSpeak = () => {     if (!speakValue) return;     const utterance: SpeechSynthesisUtterance = new SpeechSynthesisUtterance(speakValue);     utterance.voice = currentVoice as SpeechSynthesisVoice;     utterance.lang = "zh-CN";  // 使用的语言:中文     utterance.volume = 10;      // 声音音量:1     utterance.rate = 1;        // 语速:1     utterance.pitch = 1;     speechSynthesis.speak(utterance); } const handleChangeVoice = (value: string) => {     const voice = voicesList.find(d => d.voiceURI === value)     setCurrentVoice(voice) }三、结语本文内容主要为大家介绍了Web Speech API一些主要API接口的属性和方法以及相关概念,此功能某些浏览器尚在开发中,由于该功能对应的标准文档可能被重新修订,所以在未来版本的浏览器中该功能的语法和行为可能随之改变。所以该技术建议暂时不要在正式产品上使用,现阶段大家可以先去了解这个功能,待该功能稳定并有了统一的标准文档以及各个浏览器兼容性较高的情况下再酌情使用,目前如果需要使用到语音合成或者识别功能可以使用各大厂商的开放API,亦或者可以去从底层解析语音,自己开发可使用的API。

webrtc-可编程的实时音视频API

上文分享了三个视频传输协议,在分享的最后提到了一个技术就是WebRTC,在此准备分享一下对该技术的一些理解以及使用。一、什么是WebRTC WebRTC是“网络实时通信”(Web Real-Time Communication)的缩写。它最初是为了解决浏览器上视频通话而提出的,即两个浏览器之间直接进行视频和音频的通信,不经过服务器。后来发展到除了音频和视频,还可以传输文字和其他数据。在实时通信中,音视频的采集和处理是一个很复杂的过程。比如音视频流的编解码、降噪和回声消除等,但是在 WebRTC 中,这一切都交由浏览器的底层封装来完成。我们可以直接拿到优化后的媒体流,然后将其输出到本地屏幕和扬声器,或者转发给其对等端。 虽然其名为WebRTC,但是实际上它不光支持Web之间的音视频通讯,还支持Android以及IOS端,此外由于该项目是开源的,我们也可以通过编译C++代码,从而达到全平台的互通。 所以,我们可以在不需要任何第三方插件的情况下,实现一个浏览器到浏览器的点对点(P2P)连接,从而进行音视频实时通信。当然,WebRTC 提供了一些 API 供我们使用,在实时音视频通信的过程中,我们主要用到以下三个:getUserMedia                  获取音频和视频(MediaStream)RTCPeerConnection       进行音频和视频通信RTCDataChannel            进行任意数据的通信不过,虽然浏览器给我们解决了大部分音视频处理问题,但是从浏览器请求音频和视频时,我们还是需要特别注意流的大小和质量。因为即便硬件能够捕获高清质量流,CPU 和带宽也不一定可以跟上,这也是我们在建立多个对等连接时,不得不考虑的问题。二、API的使用接下来,我们通过分析上文提到的 API,来逐步弄懂 WebRTC 实时通信实现的流程。1、getUserMedia getUserMedia 这个 API 大家可能并不陌生,因为常见的 H5 录音等功能就需要用到它,主要就是用来获取设备的媒体流(即 MediaStream)。它可以接受一个约束对象 constraints 作为参数,用来指定需要获取到什么样的媒体流。let constraints = {     video: true,     audio: true } navigator.mediaDevices.getUserMedia(constraints)     .then(gotLocalMediaStream)     .catch((err) => {     console.log('getUserMedia 错误', err);     //创建点对点连接对象   });对于 constraints 约束对象,我们可以用来指定一些和媒体流有关的属性。比如指定是否获取某种流、指定视频流的宽高、帧率以及理想值,对于移动设备来说,还可以指定获取前摄像头,或者后置摄像头。更多相关设置可查看MDN我们简单看一下获取到的 MediaStream:可以看到它有很多属性,我们只需要了解一下就好,更多信息可以查看 MDN。* id [String]: 对当前的 MS 进行唯一标识。所以每次刷新浏览器或是重新获取 MS,id 都会变动。 * active [boolean]: 表示当前 MS 是否是活跃状态(就是是否可以播放)。 * onactive: 当 active 为 true 时,触发该事件。这里也可以通过 getAudioTracks()、getVideoTracks() 来查看获取到的流的某些信息,更多信息查看 MDN。2、RTCPeerConnection RTCPeerConnection 作为创建点对点连接的 API,是我们实现音视频实时通信的关键。在点对点通信的过程中,需要交换一系列信息,通常这一过程叫做 — 信令(signaling)。在信令阶段需要完成的任务:为每个连接端创建一个 RTCPeerConnection,并添加本地媒体流。获取并交换本地和远程描述:SDP 格式的本地媒体元数据。获取并交换网络信息:潜在的连接端点称为 ICE 候选者。        我们虽然把 WebRTC 称之为点对点的连接,但并不意味着,实现过程中不需要服务器的参与。因为在点对点的信道建立起来之前,二者之间是没有办法通信的。这也就意味着,在信令阶段,我们需要一个通信服务来帮助我们建立起这个连接。WebRTC 本身没有指定信令服务,所以我们可以使用但不限于使用websocket、socket、xhr等来做信令交换所需的服务。 为了完成媒体流信息的交换,我们用 RTCPeerConnection 的 createOffer() 方法生成一个 Offer,它是以 SDP(Session Description Protocol,会话描述协议)格式传送的。对方收到 Offer 后,应该生成一个 Answer 并发回,这个 Answer 同样是 SDP 格式的。通信的双方通过调用setLocalDescription() 方法,把自己生成的 SDP 设置成本地描述;通过调用setRemoteDescription() 方法,把对方发给自己的 SDP 设置成远程描述。以上的这个过程,被统称为JSEP(JavaScript Session Establishment Protocol,JavaScript 会话建立协议) 在交换SDP后,webrtc就开始真正的连接来传输音视频数据。这个建立连接的过程相当复杂,原因是webrtc既要保证高效的传输性,又要保证稳定的连通性。由于浏览器客户端之间所处的位置往往是相当复杂的,可能处于同一个内网段内,也可能处于两个不同的位置,所处的NAT网关也可能很复杂。因此需要一种机制找到一条传输质量最优的道路,而WebRTC正具备这种能力。首先简单了解以下三个概念。ICE Canidate(ICE 候选者):包含远端通信时使用的协议、IP 地址和端口、候选者类型等信息。STUN/TURN:STUN实现P2P型连接,TRUN实现中继型连接。两者实现均有标准协议。NAT穿越:NAT即网络地址转换,由于客户端并不能分配到公网IP,需要内网IP与公网IP端口做映射才能与外网通信。而NAT穿越就是位于层层Nat网关背后的客户端之间发现对方并建立连接。ICE连接大致的原理及步骤如下:发起收集ICE Canidate任务。本机能收集host类型(内网IP端口)的candidate。通过STUN服务器收集srflx类型(NAT映射到外网的IP端口)的candiate。通过TUN服务器收集relay类型的(中继服务器的 IP 和端口)的candidate。开始尝试NAT穿越,按照host类型、srflx类型、relay类型的优先级去连接。以上,WebRTC便能找到一条传输质量最优的连接道路。 当然实际情况并不是这么简单,整个过程包含着更复杂的底层细节。我们通过下图来说明WebRTC的处理步骤。初始化RTCPeerConnection:const config = { // ICE配置     iceServers: [         { urls: "stun:stun.l.google.com:19302" },  // 无需密码的         { urls: "stun:zwight.cn:3478" },         {                urls: 'turn:zwight.cn:3478',             username:"admin",             credential:"123456"         }     ], }; const createConnection = () => {         peerConnection = new RTCPeerConnection(config)         if (localStream) {             // 视频轨道             const videoTracks = localStream.getVideoTracks();             // 音频轨道             const audioTracks = localStream.getAudioTracks();             // 判断视频轨道是否有值             if (videoTracks.length > 0) {                 console.log(`使用的设备为: ${videoTracks[0].label}.`);             }             // 判断音频轨道是否有值             if (audioTracks.length > 0) {                 console.log(`使用的设备为: ${audioTracks[0].label}.`);             }             localStream.getTracks().forEach((track:any) => {                 peerConnection.addTrack(track, localStream)             })         }         // 监听返回的 Candidate         peerConnection.addEventListener('icecandidate', handleConnection);         // 监听 ICE 状态变化         peerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange)         //拿到流的时候调用         peerConnection.addEventListener('track', gotRemoteMediaStream);     } // 3.端与端建立连接     const handleConnection = (event:any) =>  {         // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象         // 获取到具体的Candidate         console.log("handleConnection", event)         const icecandidate = event.candidate;         if (icecandidate) {             socket.send(JSON.stringify({                 'userId': userId,                 'remoteId': remoteId,                 'message': {                     type: 'icecandidate',                     icecandidate: icecandidate                 }             }));         }     }     // 4.显示远端媒体流     const gotRemoteMediaStream = (event: any) => {         console.log('remote 开始接受远端流', event)         if (event.streams[0]) {             remoteVideo.srcObject = event.streams[0];         }     }创建连接://准备连接 startHandle().then(() => {    //发送给远端开启请求    socket.send(JSON.stringify({ 'userId': userId, 'remoteId': remoteId, 'message': {'type': 'connect'}})) }) const onmessage = (e: any) =>  { // websokcet         heartCheck.restart();      //简单心跳,拿到任何消息都说明当前连接是正常的         const json = JSON.parse(e.data)         const description = json.message;         console.log('message', json)         if(description.type === 'ping') return;            setRemoteId(json.userId)         const remoteId = json.userId;                  switch (description.type) {             case 'connect':                 // eslint-disable-next-line no-restricted-globals                 if(confirm(remoteId + '请求连接!')){                     //准备连接                     startHandle().then(() => {                         socket.send(JSON.stringify({ 'userId': userId, 'remoteId': remoteId, 'message': {'type': 'start'} }));                     })                 }                 break;             case 'start':                 //同意连接之后开始连接                 startConnection(remoteId)                 break;             case 'offer':                 peerConnection.setRemoteDescription(new RTCSessionDescription(description)).catch((err: any) => {                     console.log('local 设置远端描述信息错误', err);                 });                 peerConnection.createAnswer().then((answer: any) => {                     peerConnection.setLocalDescription(answer).then(() => {                         console.log('设置本地answer成功!');                     }).catch((err: any) => {                         console.error('设置本地answer失败', err);                     });                     socket.send(JSON.stringify({ 'userId': userId, 'remoteId': remoteId, 'message': answer }));                 }).catch((e: any) => {                     console.error(e)                 });                 break;             case 'icecandidate':                 // 创建 RTCIceCandidate 对象                 let newIceCandidate = new RTCIceCandidate(description.icecandidate);                 // 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中                 peerConnection.addIceCandidate(newIceCandidate).then(() => {                     console.log(`addIceCandidate 成功`);                 }).catch((error: any) => {                     console.log(`addIceCandidate 错误:\n${error.toString()}.`);                 });                 break;             case 'answer':                 peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {                     console.log('设置remote answer成功!');                 }).catch((err: any) => {                     console.log('设置remote answer错误', err);                 });                 break;             default:                 break;         }     }     //创建发起方会话描述对象(createOffer),设置本地SDP(setLocalDescription),并通过信令服务器发送到对等端,以启动与远程对等端的新WebRTC连接。     const startConnection = (remoteId: any) => {         setConnected(true);         // 发送offer         peerConnection.createOffer().then((description: any) => {             console.log(`本地创建offer返回的sdp:\n${description.sdp}`)             // 将 offer 保存到本地             peerConnection.setLocalDescription(description).then(() => {                 console.log('local 设置本地描述信息成功');                 // 本地设置描述并将它发送给远端                 socket.send(JSON.stringify({ 'userId': userId, 'remoteId': remoteId, 'message': description }));             }).catch((err: any) => {                 console.log('local 设置本地描述信息错误', err)             });         }).catch((err: any) => {             console.log('createdOffer 错误', err);         });     }以上为本实例部分主要代码,主要使用了websokcet作为信令交换服务器的连接桥梁,WebRTC在内网中无需依赖STURN中继服务器即可实现端到端的连接。详细代码请转这里3、RTCDataChannelRTCDataChannel的作用是在点对点之间,传播任意数据。它的API与WebSockets的API相同,RTCPeerConnection他进行强大和灵活的端到端通讯。通讯直接发生在浏览器之间.所以RTCDataChannel在使用了TURN中继服务来应对网络穿透失败的情况下还可以比WebSocket更快。var peerConnection = new webkitRTCPeerConnection(servers,   {optional: [{RtpDataChannels: true}]}); peerConnection.ondatachannel = function(e) {   receiveChannel = e.channel;   receiveChannel.onmessage = function(event){     console.log(event)   }; }; sendChannel = peerConnection.createDataChannel("sendDataChannel", {reliable: false}); sendChannel.send(data);三、WebRTC发展和总结 2010年5月,谷歌以6820万美元收购GIPS。当然谷歌不光收购了GIPS,还收购了On2,得到了VPx系列视频编解码器。于是在2011年,webrtc项目诞生,融合了GIPS的音视频引擎、VPx视频编解码器,P2P穿洞技术等,而且开源。 经历了6年的时间,2017 年 11 月 2 日 ,W3C WebRTC 1.0 草案正式定稿,webrtc加入W3C大家族。随后,各大浏览器厂商跟紧支持。 2021 年 1 月 26 日,W3C(万维网联盟) 和 IETF (互联网工程任务组) 同时宣布 WebRTC(Web Real-Time Communications,Web 实时通信) 现发布为正式标准。 由于WebRTC的传输是基于公共互联网,而公共互联网并不是为了实时通信而设计的,因此在网络协议、跨区域带宽、跨运营商、用户设备、网络架构、文档支持等方面都会对WebRTC的开发有牵制,从而会导致实时音视频等传输质量没办法得到有效的保证。因此,可以说如果 WebRTC 直接拿过来商用的话,几乎是不太可能的,当下普遍的解决方案是自研,根据自身的业务场景进行二次定制开发,或者更简单一点使用第三方 SDK。本文示例源码库: webrtc

视频流传输协议(RTSP、RTMP、HLS)

视频相关的协议有很多,不同的公司,甚至有自己的协议标准。本文介绍的协议是本人开发中接触较多的几个,主要从前端对接的角度去了解和使用以及比对不同协议的优缺点。1、RTSP RTSP(Real Time Streaming Protocol)实时流传输协议,是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学、网景和RealNetworks公司提交的IETF RFC标准。该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP在体系结构上位于RTP(Realtime Transport Potocol 实时传输协议:提供时间标志,序列号以及其他能够保证在实时数据传输时处理时间的方法)和RTCP(Realtime Transport Control Potocol 实时传输控制协议:是RTP的控制部分,用来保证服务质量和成员管理。RTP和RTCP是一起使用的)之上,它使用TCP或UDP完成数据传输,rtsp负责建立和控制会话,rtp负责多媒体的传输,rtcp配合rtp做控制和流量统计,他们是合作的关系 RTSP具体数据传输交给RTP,提供对流的远程控制,RTP是基于 UDP协议的, UDP不用建立连接,效率更高;但允许丢包, 这就要求在重新组装媒体的时候多做些工作,RTP只是包裹内容信息,而RTCP是交换控制信息的,Qos是通过RTCP实现的应用程序对应的是play, seek, pause, stop等命令,RTSP则是处理这些命令,在UDP传输时并使用RTP(RTCP)来完成。如果是TCP连接则不会使用RTP(RTCP)。 RTSP传输协议的视频流地址主要由rtsp://开头,对于网络摄像头提供的RTSP视频流一般使用的地址如rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov。开发过程中他的缺点是H5原生不支持RTSP协议,所以在H5开发中无法使用RTSP协议进行视频流的播放,因此面对需要在H5中对接RTSP协议的视频流的情况下只能通过服务端去转换视频流协议实现。2、RTMP RTMP(Real Time Message Protocol)实时信息传输协议,是Adobe公司为Flash播放器和服务器之间提供音视频数据传输服务而设计的应用层私有协议,RTMP协议主要的特点有:多路复用,分包和应用层协议。2.1、多路复用 多路复用(multiplex)指的是信号发送端通过一个信道同时传输多路信号,然后信号接收端将一个信道中传递过来的多个信号分别组合起来,分别形成独立完整的信号信息,以此来更加有效地使用通信线路。 简而言之,就是在一个 TCP 连接上,将需要传递的Message分成一个或者多个 Chunk,同一个Message 的多个Chunk 组成 ChunkStream,在接收端,再把 ChunkStream 中一个个 Chunk 组合起来就可以还原成一个完整的 Message,这就是多路复用的基本理念2.2 、分包 RTMP协议的第二个大的特性就是分包,与RTSP协议相比,分包是RTMP的一个特点。与普通的业务应用层协议不一样的是,在多媒体网络传输案例中,绝大多数的多媒体传输的音频和视频的数据包都相对比较偏大,在TCP这种可靠的传输协议之上进行大的数据包传递,很有可能阻塞连接,导致优先级更高的信息无法传递,分包传输就是为了解决这个问题而出现的。RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发 RTMP相较于RTSP来说以前浏览器对它的支持比较好,能应用于浏览器,加载flash插件后就能直接播放,但是随着adobe公司宣布不再维护更新flash后,chrome等浏览器也逐渐放弃对flash的依赖,最终在2020年的12月彻底移除flash,因此RTMP协议视频流也无法在不支持flash的浏览器上播放播放RTMP需要使用第三方库进行播放,当前使用的是videojs。<link href="./video-js.css" rel="stylesheet" /> <script src="./video.js"></script> <video id="my-video" class="video-js" controls autoplay preload="auto" style="width: 800px; height: 400px">    <!-- rtmp://rtmp01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0 --> <source src="rtmp://mobliestream.c3tv.com:554/live/goodtv.sdp" type="rtmp/flv" /> <p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a> </p> </video>3、HLS HLS(HTTP Live Streaming)是苹果公司提出的基于HTTP的流媒体网络传输协议。类似于MPEG-DASH,但是HLS更加简洁,它的基本原理也是服务端把文件或媒体流按照不同的码率切分成一个个小片段进行传输,客户端在播放码流时,可以根据自身的带宽及性能限制,在同一视频内容的不同码率的备用源中,选择合适码率的码流进行下载播放。在传输会话开始时,客户端首先需要下载描述不同码流元数据的M3U8索引文件(类似于DASH中的MPD文件)。 HLS的出现主要是为了解决RTMP协议存在的一些问题。比如RTMP协议不使用标准的HTTP接口传输数据,所以在一些特殊的网络环境下可能被防火墙屏蔽掉。但是HLS由于使用的HTTP协议传输数据,不会遇到被防火墙屏蔽的情况 另外对于负载,RTMP是一种有状态协议,很难对视频服务器进行平滑扩展,因为需要为每一个播放视频流的客户端维护状态。而HLS基于无状态协议(HTTP),客户端只是按照顺序使用下载存储在服务器的普通TS文件,做负责均衡如同普通的HTTP文件服务器的负载均衡一样简单。 虽然HLS有上述优势,但也同时存在延迟过大的劣势。采用HLS直播的视频流延时一般在10秒以上,而RTMP直播的延迟最低可达到3、4秒,因此,在对实时性要求较高的场合,如互动直播,就要慎用HLS了。3.1、HLS流程根据媒体流的生成及流向,HLS的结构可划分为如下几个部分: 从左到右讲,左下方的inputs的视频源是什么格式都无所谓,他与server之间的通信协议也可以任意(比如RTMP),总之只要把视频数据传输到服务器上即可。这个视频在server服务器上被转换成HLS格式的视频(既TS和m3u8文件)文件。细拆分来看server里面的Media encoder的是一个转码模块负责将视频源中的视频数据转码到目标编码格式(H264)的视频数据,视频源的编码格式可以是任何的视频编码格式。转码成H264视频数据之后,在stream segmenter模块将视频切片,切片的结果就是index file(m3u8)和ts文件了。图中的Distribution其实只是一个普通的HTTP文件服务器,然后客户端只需要访问一级index文件的路径就会自动播放HLS视频流了。3.2、M3U8文件描述#EXTM3U #EXT-X-VERSION:3 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE:1 #EXT-TS-OFFSET-BEGIN:1 #EXT-X-DISCONTINUITY #EXTINF:1.997, http://whhls4.ys7.com:7888/openlivedata/E68018732_1_2/a4e0dbf80fed42068987b1f140b19bf6-1.ts?Usr=8dfa3df53cef47c1b712aa70475d1870 #EXT-TS-OFFSET-BEGIN:3 #EXTINF:1.997, http://whhls4.ys7.com:7888/openlivedata/E68018732_1_2/a4e0dbf80fed42068987b1f140b19bf6-2.ts?Usr=8dfa3df53cef47c1b712aa70475d1870 #EXT-TS-OFFSET-BEGIN:5 #EXTINF:1.996, http://whhls4.ys7.com:7888/openlivedata/E68018732_1_2/a4e0dbf80fed42068987b1f140b19bf6-3.ts?Usr=8dfa3df53cef47c1b712aa70475d1870#EXTM3U:每一个M3U文件第一行必须是这个tag,请标示做用#EXT-X-VERSION:用以标示协议版本#EXT-X-ALLOW-CACHE:是否允许cache#EXT-X-TARGETDURATION:指定最大的媒体段时间长(秒)。所以#EXTINF中指定的时间长度必须小于或是等于这个最大值#EXT-X-MEDIA-SEQUENCE:每一个media URI 在 PlayList中只有唯一的序号,相邻之间序号+1,默认为0#EXT-TS-OFFSET-BEGIN:当前视频的时间戳是(相对首次播放时间)#EXT-X-DISCONTINUITY:表明其前一个切片与下一个切片之间存在中断,当文件格式、数字、类型、媒体标识符等变化时需要使用该标识符#EXTINF:指定每个媒体段(ts)的持续时间,这个仅对其后面的URI有效,每两个媒体段URI间被这个tag分隔开更多M3U8文件属性不在这里详细说明,需要了解的可以自行了解。3.3、HLS的使用HLS是提供一个m3u8地址,Apple的Safari浏览器直接就能打开m3u8地址,譬如:http://hls01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0.m3u8 Android不能直接打开,需要使用html5的video标签,然后在浏览器中打开这个页面即可,譬如: <video style="width: 100%;"     autoplay controls autobuffer muted     playsInline webkit-playsinline     src="http://hls01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0.m3u8"     type="application/vnd.apple.mpegurl"> </video> PC端播放HLS需要通过转换后才能播放,当前所选择的是hls.js,它通过将MPEG-2传输流和AAC / MP3流转换为ISO BMFF(MP4)片段,实现如下: <body>     <video id="video" controls playsInline webkit-playsinline muted></video> </body> <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <script>     (function(){         var Hls = window.Hls;         var url = 'http://hls01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0.m3u8';         if (Hls.isSupported()) {             var hls = new Hls();             hls.loadSource(url);             hls.attachMedia(video); // 将hls stream attach到video             // 监听MANIFEST_PARSED事件,通知video开始播放             hls.on(Hls.Events.MANIFEST_PARSED, function () {                 video.play();             })         } else if (video.canPlayType('application/vnd.apple.mpegurl')) {             video.src = url;             video.addEventListener('canplay', function () {                 video.play();             })         }     })() </script>4、协议转换 一般来说网络摄像头输出的视频流rtsp会占较多数,例如海康、大华等摄像头厂商。那面对输出的rtsp流但是又需要再浏览器端播放实时监控我们又该如何呢,在这种情况下我们需要对视频流进行转换,比如将rtsp转换为rtmp或hls、rtmp转换为hls等。而协议之间的恶转换需要用到工具,本文使用到的是FFMPEG,它是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序,提供了录制、转换以及流化音视频的完整解决方案。FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、[Mac OS X](https://baike.baidu.com/item/Mac OS X/470629)等。4.1、环境安装 需要转换视频流并能使用我们需要搭建一个简易的流媒体服务,这里我们使用nginx搭建流媒体服务并提供视频流的转发。首先我们需要安装nginx并添加nginx 的rtmp module,通过rtmp module可以将nginx和ffmpeg组合成一个功能相对比较完善的流媒体服务器。nginx安装命令:brew install nginx-full --with-rtmp-module;ffmpeg安装命令:brew install ffmpegnginx配合ffmpeg做流媒体服务器的原理是: nginx通过rtmp模块提供rtmp服务, ffmpeg推送一个rtmp流到nginx,然后客户端通过访问nginx来收看实时视频流. HLS也是差不多的原理,只是最终客户端是通过HTTP协议来访问的,但是ffmpeg推送流仍然是rtmp的。4.2、nginx配置在nginx.conf的http后面添加如下配置:rtmp {     server {      listen 7001;      # RTMP 直播流配置      application rtmplive {        live on;      }      # HLS 直播流配置      application hls {          live on; # 开启实时          hls on; #开启hls          hls_path /Users/edy/Desktop/self/nginx/live;          hls_fragment 2s; #一个ts文件的时长2s          hls_playlist_length 6s; # HLS播放列表长度      }    } }配置HLS拉流,需要在http中添加一个locationlocation /live {     types { # 指定格式         application/vnd.apple.mpegurl m3u8;         video/mp2t ts;     }     root   /Users/edy/Desktop/self/nginx;     add_header Cache-Control no-cache; }配置完成后重启nginx。4.3、FFmpeg推流测试RTMP流,推流至rtmplive:ffmpeg -re -i rtmp://rtmp01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0 -vcodec libx264 -acodec aac -g 48 -ar 44100 -ac 1 -f flv -s 1920x1080 rtmp://127.0.0.1:7001/rtmplive/test1HLS流,推流至hls:ffmpeg -re -i rtmp://rtmp01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0 -vcodec libx264 -acodec aac -g 48 -ar 44100 -ac 1 -f flv -s 1920x1080 rtmp://127.0.0.1:7001/hls/test2rtsp和rtmp作为来源流他们的推流方式一致,rtsp推流如下:ffmpeg -re -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec libx264 -acodec aac -g 48 -ar 44100 -ac 1 -f flv -s 1920x1080 rtmp://127.0.0.1:7001/hls/test2现在我们的流媒体服务器就有两个实时流了,一个是rtmp的,一个是hls的;拉流地址如下RTMP流:rtmp://127.0.0.1:7001/rtmplive/test1HLS流:http://127.0.0.1:7071/live/test2.m3u8在测试推流测试阶段,HLS流表现较明显,在nginx的临时目录下,直观的可看到m3u8索引文件和N多个.ts文件。m3u8列表会实时更新,且会动态更改当前播放索引切片(.ts)。这种实时更新的机制,不会使得.ts文件长时间存在于Nginx服务器上,且当推流结束之后,该目录下的内容会被全部清除,这样无形中减缓了nginx服务器的压力。当然FFmpeg也可以将rtmp或其他的视频转为本地的hls保存在本地而不被清除:ffmpeg -re -i rtmp://rtmp01open.ys7.com/openlive/7ebec024f34540f4be99d4f20c72f8b0 -vcodec libx264 -acodec aac -g 48 -ar 44100 -ac 1 -f segment -segment_list /Users/edy/Desktop/self/nginx/mu/playlist.m3u8 -segment_list_flags +live -segment_time 10 /Users/edy/Desktop/self/nginx/mu/out%03d.tsHLS流:http://127.0.0.1:7071/mu/playlist.m3u8FFmpeg配置项描述FFmpeg 的命令行参数非常多,可以分成五个部分ffmpeg {1} {2} -i {3} {4} {5}全局参数输入文件参数输入文件输出文件参数输出文件 -re: 将输入的读取速度降低到输入的本地帧速率 -i:输入文件,后面接我们的输入源地址 -vcodec libx264:强制使用libx264编解码方式 -acodec aac:音频使用codec编解码 如:-acodec AAC 使用AAC音频编码 -g 48设置图像组大小 这里设置GOP大小,也表示两个I帧之间的间隔(若不设置或设置为0可能会导致VLC播放输出的rtmp没有图像只有声音) -ar 44100:设置音频采样率 -ac 1:设置音频通道 缺省为1,即单通道 -f flv:强制输出视频流采用格式flv -s 1920x1080:设置帧大小 格式为WXH 缺省160X128 

  • 1