以异步模式打开实时播放
当应用要同时打开多个终端通道时,应该使用异步模式打开。本示例演示异步实时播放的一般编程模式。可结合示例页面:live-example-2(请使用Chrome/Firefox/Edge等浏览器打开),及其代码:live-example-2.zip进行本文的阅读。
本示例使用以下到库:
同、异步模式的主要差异
异步模式和同步模式的主要差异在于:
模式 | 接口参数 | 返回后码流是否已可用 | 可确认终端不在线 | 需要订阅WebSocket通知 |
---|---|---|---|---|
同步 | async : false | 是 | 可确认 | 可选 |
异步 | async : true | 不一定 | 不可确认 | 必需 |
以下逐一解释这些差异:
- 调用
POST /strm/live/open
接口打开通道时,我们需要传递一个true
值作为async
的参数,告诉服务端我们要使用异步模式:
axios.post(apiUrlPrefix() + 'strm/live/open', {
'simNo': simNo(),
'channel': channelId(),
'dataType': dataType(),
'codeStream': codeStream(),
'proto': 0,
'async': true // <--- 异步模式
}, {
headers: {
'X-Auth-Token': token
}
}).then(function (resp) {
// ...
}).catch(function (err) {
// ...
});
- 异步模式的
POST /strm/live/open
在向媒体服务分配一个播放请求后、下发终端指令以前返回,而同步模式则在码流已经准备好或确认终端不在线时返回。 异步模式接口调用返回后,服务端继续进行终端交互,指示终端向媒体服务推流。故当POST /strm/live/open
调用返回后,如果我们立即创建播放器并加载码流, 播放器通常处于等待码流的状态,除非通道已经有其他用户正在打开。
axios.post(apiUrlPrefix() + 'strm/live/open', {
'simNo': simNo(),
'channel': channelId(),
'dataType': dataType(),
'codeStream': codeStream(),
'proto': 0,
'async': true
}, {
headers: {
'X-Auth-Token': token
}
}).then(function (resp) {
// ...
// 可以直接创建播放器并等待码流。播放器通常会处于等待码流的状态,除非通道已经有其他用户正在打开
createPlayerAndLoad(data.playUrl, data.mediaTyp);
// ...
}).catch(function (err) {
// ...
});
- 同步模式调用
POST /strm/live/open
时,服务端在完成终端交互后返回,当出现终端不在线时,接口返回-27: 没有可用的连接(终端不在线)
的错误码。 而在异步模式下,服务端在进行终端交互之前返回,故而接口调用无法确认终端是否在线,此时需要通过websocket API来订阅相关通知来作相应的处理。 - 异步模式需要订阅WebSocket API的
user/queue/strm
消息来接收通道状态变化的相关通知,并作相应的处理,主要收到closed
事件时,我们要做关闭播放器、停止保持定时器等工作:
// 判断事件类型并作相应的处理
switch (notif.act) {
// ...
case StrmNotif.ACT__strmClosed:
if (notif.closeCause !== StrmNotif.CLOSE_CAUSE__clientReq) {
errLog(closeReasonOf(notif.closeCause));
} else
infoLog('音视频通道已经关闭');
// 收到 `closed`事件通知时,关闭播放器
stop();
break;
}
订阅WebSocket通知
WebSocket接口地址
WebSocket地址可从HTTP接口地址中派生,只需将HTTP接口地址的https
变为wss
或将http
变为ws
,然后在后面加上/ws
和__token
参数即可。如API地址为:
https://n11.gratour.info:8388/v1
则WebSocket接口地址为:
wss://n11.gratour.info:8388/v1/ws?__token=Ge4E1xNHSfW8NYa0VJe48A
上面地址示例中的__token
参数的参数值为POST /login
接口返回的authToken
属性的值。
见示例代码中的connectWs()
函数:
/**
* 连接到websocket并监听 StrmNotif 事件
*/
function connectWs() {
// 从HTTP API地址中派生 websocket 接口地址
let url = apiUrlPrefix();
url = url.replace('https://', 'wss://').replace('http://', 'ws://');
if (!url.endsWith('/v1/'))
url += 'v1/';
url += 'ws?__token=' + token; // 用 `POST /login`接口返回的`authToken`作为`__token`参数
stomp = Stomp.client(url);
// ...
}
有关WebSocket的基本约定,请参考:基本约定
连接WebSocket的时机和重连
一旦登录成功,我们就可以连接WebSocket并订阅需要监听的事件:
infoLog('登录到:' + apiUrlPrefix());
axios.post(apiUrlPrefix() + 'login', loginReq)
.then(function (resp) {
token = apiCheck(resp).data[0].authToken;
// 更新最后成功调用API的时间
lastApiTime = new Date().getTime();
// 连接websocket进行事件监听
connectWs();
// ...
})
.catch(function (err) {
// ...
});
如果WebSocket因网络原因断开时,WebSocket客户端的错误处理函数将被调用,我们可以在这个处理函数中进行一个延时的重连处理。 但在重连处理中有一点需要注意的是,WebSocket会话的有效性依赖于token的有效性。当token失效时(如长时间无HTTP API调用),WebSocket会话也会失效,WebSocket客户端也会发生错误,从而触发错误处理函数被调用。 因此,在WebSocket客户端的错误处理函数中,要先确定token的有效性,只在认为token仍有效的情况下才尝试重连。
由于token的失效主要原因是长时间无HTTP API调用,所以为了确定token有效性,可以引入一个记录最后一次成功调用HTTP API的时间的变量,每次成功调用API后,将变量设为当前时间;判断时,如果当前时间距离上次成功调用API时间超过10分钟(此时间值可配置),则认为token已经无效。
示例代码中的lastApiTime即为记录最后一次成功调用API的时间变量:
/**
* 连接到websocket并监听 StrmNotif 事件
*/
function connectWs() {
// ...
stomp.connect(
// 连接参数,未使用
{},
// 连接成功回调
function (_) {
// ...
},
// 错误回调
function (err) {
const message = 'Websocket发生错误:' + err.toString();
errLog(message);
disconnectWs();
if (token) {
const sinceLastApiCall = new Date().getTime() - lastApiTime;
if (sinceLastApiCall < 10 * 60 * 1000) {
// 如果上次成功调用API是在10分钟前,则token依然是有效的,此种情况下我们重连websocket(5秒后)
setTimeout(() => connectWs(), 5000);
}
}
}
);
}
订阅流媒体状态通知
WebSocket通知的订阅地址不是固定地址,其中一个包含token
占位符,实际使用时替换为POST /login
接口返回的authToken
的值。例如,流媒体状态通知的订阅地址模式为:
/user/{token}/queue/strm
假设登录后得到的authToken
为Ge4E1xNHSfW8NYa0VJe48A
,那么订阅地址为:
/user/Ge4E1xNHSfW8NYa0VJe48A/queue/strm
代码中的相关部分为:
/**
* 连接到websocket并监听 StrmNotif 事件
*/
function connectWs() {
// 从HTTP API地址中派生 websocket 接口地址
let url = apiUrlPrefix();
url = url.replace('https://', 'wss://').replace('http://', 'ws://');
if (!url.endsWith('/v1/'))
url += 'v1/';
url += 'ws?__token=' + token; // 用 `POST /login`接口返回的`authToken`作为`__token`参数
stomp = Stomp.client(url);
stomp.connect(
// 连接参数,未使用
{},
// 连接成功回调
function (_) {
stompConnected = true;
// 队列名称构成方式:/user/{token}/queue/strm
const queueName = '/user/' + token + '/queue/strm';
// 订阅 StrmNotif 事件
stomp.subscribe(queueName, (frame) => {
// 事件处理
// ...
});
},
// 错误回调
function (err) {
// ...
}
);
}