跳到主要内容
版本:3.4.0

HLS的实时播放示例

本示例以html+js构造一个简单的实时HLS音视频播放页面应用为例,演示Micro-GNSSAPI的调用如何与HLS的播放结合。 可结合示例页面:live-example-3(请使用Chrome/Firefox/Edge等浏览器打开), 及其代码:live-example-3-hls.zip进行本文的阅读。

本示例主要使用到以下库:

关于 Micro-GNSS API的调用流程或FLV格式流的播放,可以参考 实时播放示例,本文主要介绍HLS格式流的播放。

HLS在不同浏览器上的播放

iOS和macOS的Safari浏览器,以及在Android手机上的新版Chrome浏览器原生支持HLS媒体流的播放,但其它浏览器,如Firefox、桌面版的Chrome、Edge, 却不支持HLS的原生播放,在这些浏览器上播放HLS,需要借助这些浏览器的 Media Source Extension 功能来进行播放,而 hls.js 就是利用 Media Source Extension 功能来进行播放的JS播放器。

因此,为了能够在不同浏览器上播放HLS媒体流,我们可以考虑使用原生播放器优先的策略:

  • 先检查浏览器是否原生支持播放HLS,如支持,则使用原生播放器来播放
  • 否则,使用 hls.js 库(如果浏览器原生支持 Media Source Extension 的话)

又或者使用 Media Source Extension 优先的策略:

  • 先检查浏览器是否支持Media Source Extension,如果支持,则使用 hls.js 库来进行播放
  • 否则,使用原生播放器来播放 (如果浏览器原生支持 HLS 的话)

检查浏览器是否原生支持 HLS

我们可以用 HtmlMediaElement的 canPlayType 方法来检查浏览器是否原生支持 HLS,如示例代码中的:

let nativeSupported = videoElement.canPlayType('application/vnd.apple.mpegurl');

检查浏览器是否支持 hls.js

直接调用 Hls.isSupported()方法即可:

let hlsJsSupported = Hls.isSupported();

HLS播放器的抽象

为了让主程序更加清晰,不过多牵涉细节,在本示例中,我们定义一个抽象的 HLS 播放器:

/**
* HLS播放器的抽象
*/
class AbstractPlayer {


/**
*
* @param {string} url playUrl
* @param {string} mediaTyp media type, one of `av`, `a`, `v`
* @param {function : void} onCloseCallback
*/
constructor(url, mediaTyp, onCloseCallback) {
this.playingUrl = url;
this.mediaTyp = mediaTyp;

this.loaded = false;
this.onCloseCallback = onCloseCallback;
}

/**
* 内部加载方法,具体类需要实现此方法
*/
_internalLoad() {
}

/**
* 加载媒体流
*/
load() {
this._internalLoad();
this.loaded = true;
}

/**
* 内部关闭方法,具体类需要实现此方法
*/
_internalClose() {
}

/**
* 关闭播放器
*/
close() {
if (this.loaded) {
this._internalClose();
this.loaded = false;
}

this.loaded = false;
if (this.onCloseCallback)
this.onCloseCallback();
}
}

原生播放器(NativePlayer)的实现

在使用原生播放器时,我们定义一个名为 NativePlayer 的具体类,它的构造主要设置 HtmlVideoElement的src属性和onerror事件:

class NativePlayer extends AbstractPlayer {

/**
*
* @param {string} url playUrl
* @param {string} mediaTyp media type, one of `av`, `a`, `v`
* @param {function : void} onCloseCallback
*/
constructor(url, mediaTyp, onCloseCallback) {
super(url, mediaTyp, onCloseCallback);

// 将 src 属性设置为媒体地址
videoElement.src = url;

// 设置 onerror事件,使发生错误时关闭播放器
videoElement.onerror = (event) => {
errLog(`加载 ${url} 时遇到错误:${JSON.stringify(event, null, 2)}`);
this.close();
};

// ...
}
}

加载时,调用 HtmlVideoElement的load方法,然后再调用 play方法:

class NativePlayer extends AbstractPlayer {

// ...

_internalLoad() {
// load
videoElement.load();

// and then play
videoElement.play().then(() => {
setStateText('播放中...');
}).catch((err) => {
errLog(`播放时遇到错误:${JSON.stringify(err, null, 2)}`);
this.close();
});
}

// ...
}

关闭时,调用 HtmlVideoElement的pause方法,然后再移除src属性:

class NativePlayer extends AbstractPlayer {

// ...

_internalClose() {
videoElement.pause();
videoElement.removeAttribute('src');
}

// ...
}

hls.js 实现 (HlsPlayer)

在使用原生播放器时,我们定义一个名为 NativePlayer 的具体类,它的构造主要创建内部的hlsPlayer实例并 attach 到指定的 HtmlVideoElement 上,然后设置 MANIFEST_PARSED 事件回调和 ERROR 事件回调:

class HlsPlayer extends AbstractPlayer {

/**
*
* @param {string} url playUrl
* @param {string} mediaTyp media type, one of `av`, `a`, `v`
* @param {function : void} onCloseCallback
*/
constructor(url, mediaTyp, onCloseCallback) {

// ...

// 创建内部的 hlsPlayer 实例
this.hlsPlayer = new Hls(config);
// attach 到指定的 HtmlVideoElement
this.hlsPlayer.attachMedia(videoElement);

// 在接收到码流元数据后开始播放
this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
videoElement.play().then(() => {
setStateText('播放中...');
}).catch((err) => {
errLog('播放时遇到错误:' + err.toString());
this.close();
});
infoLog('已经收到媒体元数据');
});

// 添加错误处理器
this.hlsPlayer.on(Hls.Events.ERROR, (_, data) => {
const errType = data.type;
const errDetails = data.details;
const errFatal = data.fatal;

console.log(`Error occurred: ${JSON.stringify(data, null, 2)}.`);

if (errFatal) {
switch (errType) {
case Hls.ErrorTypes.NETWORK_ERROR:
infoLog('遇到网络错误,尝试恢复');
this.hlsPlayer.startLoad();
break;

case Hls.ErrorTypes.MEDIA_ERROR:
infoLog('遇到媒体错误,尝试恢复');
this.hlsPlayer.recoverMediaError();
break;

default:
// cannot recover
errLog('播放时发生错误:' + errType + ', detail=' + errDetails);
this.close(); // 关闭keep定时器,关闭播放器
}
}
});
}

// ...
}

加载时,调用 Hls 的 loadSource 方法:

class HlsPlayer extends AbstractPlayer {

// ...

_internalLoad() {
// 加载码流, just load, not play yet
this.hlsPlayer.loadSource(this.playingUrl);
}

// ...
}

关闭时,调用 Hls 的 destory 方法:

class HlsPlayer extends AbstractPlayer {

// ...

_internalClose() {
this.hlsPlayer.destroy();
this.hlsPlayer = null;
}

// ...
}

播放器的创建和使用

    /**
* 创建播放器并加载码流
*
* @param {string} url the playUrl
* @param mediaTyp media type, ignored when we use HLS player
*/
function createPlayerAndLoad(url, mediaTyp) {
if (!hlsJsSupported && !nativeSupported) {
errLog('当前浏览器不支持播放HLS格式的媒体流');
return;
}

if (hlsJsSupported) {
player = new HlsPlayer(url, mediaTyp, onPlayerClosed);
infoLog('使用hls.js播放器');
} else {
player = new NativePlayer(url, mediaTyp, onPlayerClosed);
infoLog('使用原生播放器');
}

player.load();

setStateText('加载中...');
infoLog('加载:' + url);
}

API调用上与 FLV 的不同

HLS媒体应用在API调用上与 FLV 的不同几乎没有差别,只是在调用 /strm/live/open 时,proto参数应设置 1

        axios.post(apiUrlPrefix() + 'strm/live/open', {
simNo: sn,
channel: channelId(),
dataType: 0, // 目前只支持音视频,不支持仅视频或仅音频
codeStream: codeStream(),
proto: 1, // 客户端协议: HLS
async: true
}, {
headers: {
'X-Auth-Token': token
}
}).then(function (resp) {
// ...
}).catch(function (err) {
// ...
});