跳到主要内容
版本:1.1.7

初步:实时播放

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

本示例使用以下到库:

基本流程

img_1.png

连接终端

准备一台符合JT/T 1076标准的终端,记下终端的通讯标识号(即协议中所说的终端手机号,以下假设为13320200317)。将终端的服务器地址设为:n11.gratour.info,端口号设为: 7601。 如果终端设置了ACC ON才响应音视频指令的话,则应接上ACC ON信号线。

等待终端上线

启动终端后,接下来我们就是等待终端上线。可到 https://n11.gratour.info:3001/ 查看是否已经上线。如下图示,我们在在线终端列表中看到终端13320200317,表示该终端已经上线。

term-online.png

调用登录接口

终端上线后,我们就可以通过API来对终端进行操作。本示例使用测试服务器的API( https://n11.gratour.info:8388/ )来进行操作。 API操作的第一步,就是用用户名/密码(admin/admin)调用POST /login接口进行登录(参见示例代码中apiLogin()函数),如下:

POST https://n11.gratour.info:8388/v1/login
Content-Type: application/json

{
"userName":"admin",
"password": "admin"
}

得到应答:

{
"data": [
{
"authToken": "w3-73jH2TWq4cmYKGuCWMw",
"ver": "1.1.4"
}
],
"count": 1,
"errCode": 0,
"message": "成功。"
}

应答中的data[0].authToken为登录令牌,后续的调用中均需要带上此令牌。

调用打开实时音视频接口

登录成功后,再调用POST /strm/live/open接口(参见示例代码中的apiLiveOpen()函数),打开音视频通道,如下:

        axios.post(apiUrlPrefix() + "strm/live/open", {
"simNo": simNo(),
"channel": channelId(),
"dataType": dataType(),
"codeStream": codeStream(),
"proto": 0 // 客户端协议: HTTP-FLV
}, {
headers: {
'X-Auth-Token': token
}
}).then(function (resp) {
/**
* @type {{reqId: string, playUrl: string, mediaTyp: string}}
*/
let data = apiCheck(resp).data[0];

// 得到请求ID(reqId)后,保存起来,后续关闭通道时要用到
reqId = data.reqId;

setStateText('流已经创建');

// 启动保持定时器,间隔:15秒
keepAliveTimer = setInterval(apiKeepAlive, 15000);

// 创建播放器并加载码流
createPlayerAndLoad(data.playUrl, data.mediaTyp);

setButtonEnabled(btnStop, true);
setButtonEnabled(btnAvLiveReq, false);
}).catch(function (err) {
alert(err);
});

注意请求中带上了X-Auth-Token头部。

POST /strm/live/open接口的功能是打开指定的终端音视频通道。所谓的打开指定的终端音视频通道,就是指示终端向服务端推送指定的通道的音视频,然后服务端在检测到终端推流后,返回可供客户端播放的流媒体地址。

实际发出的请求示例:

POST https://n11.gratour.info:8388/v1/strm/live/open
Content-Type: application/json
X-Auth-Token: w3-73jH2TWq4cmYKGuCWMw

{
"simNo": "13320200317",
"channel": 1,
"dataType": 0,
"codeStream": 1,
"proto": 0
}

调用后,服务端将向终端下发音视频传输请求(9101)指令。当终端开始推流,服务端返回播放地址,类似于:

{
"data": [
{
"reqId": "AbctuB9sSJe8bbBzv-yr9g",
"ctrl": true,
"playUrl": "https://n11.gratour.info:20022/s/13305071289_1_1?a\u003dS7rAwnY0\u0026b\u003dS91-lPEJg_yulbO6okcRQrguBqEFlUH-YmSXQewOJYSpWg8_7Jx2xZdBYiNZhWZETYFmZ3Kd8j_qguPRf10Yfvl_WB8H5tFUHkh6nILIiZU6wKN3buuvaXAKNA93QmIsk77baWYzkuY70l9TQswdvRRNOBXz9fszrWeQ5aeca6B9PXXUw_WVZiQBiUfrgF7CS2N\u0026reqId\u003dM2MyM2FmMTA3NDQyNGE1YzhlY2VkMzc3ZWU3ZDEwYTQ",
"mediaTyp": "av",
"ready": true
}
],
"count": 1,
"errCode": 0,
"message": "OK."
}

以上应答返回的data[0]中,有几个我们需要注意的属性:

  • reqId:请求ID,表示服务分配给本次音视频请求的ID,此ID在后面调用POST /keep_alivePOST /strm/live/ctrl接口时需要用到
  • playUrl:播放地址。我们可以直接用flv.js打开此地址进行播放。
  • mediaTyp:媒体码流的音视频属性,有三个可能值,用于初始化flv.js的播放器的hasAudio/hasVideo设置:
    • av:表示码流包含音频和视频
    • a:表示码流仅包含音频
    • v:表示码流仅包含视频
  • ready: 流是否已经准备好,由于本例使用的是同步调用模式,所以ready总是为true

启动保持定时器

在前面的POST /strm/live/open请求成功返回后,客户端应立即启动一个间隔为15秒的保持定时器,每当定时器到时时,用请求ID(reqId)调用POST /keep_alive,以保持通道的活跃。


/**
* 打开实时音视频
*/
function apiLiveOpen() {

// ......

axios.post(apiUrlPrefix() + "strm/live/open", {
"simNo": simNo(),
"channel": channelId(),
"dataType": dataType(),
"codeStream": codeStream(),
"proto": 0 // 客户端协议: HTTP-FLV
}, {
headers: {
'X-Auth-Token': token
}
}).then(function (resp) {
/**
* @type {{reqId: string, playUrl: string, mediaTyp: string}}
*/
let data = apiCheck(resp).data[0];

// 得到请求ID(reqId)后,保存起来,后续关闭通道时要用到
reqId = data.reqId;

// ......

// 启动保持定时器,间隔:15秒
keepAliveTimer = setInterval(apiKeepAlive, 15000);

// ......
}).catch(function (err) {
alert(err);
});
}


/**
* 流请求保持
*/
function apiKeepAlive() {
if (!token)
return;

axios.post(apiUrlPrefix() + "strm_media/keep_alive", {reqIds: [reqId]}, {
headers: {
'X-Auth-Token': token
}
})
.then((resp) => {
if (resp.data.errCode !== 0) {
errLog('保持失败,通道已经关闭:' + resp.data.errCode + '-' + resp.data.message);
stop();
changeStateToClosed();
} else {
console.log("KeepAlive sent");
}
})
.catch((err) => {
errLog('调用保持接口时遇到错误:' + err.toString());
})
}

注意:如果没有及时调用POST /keep_alive接口,通道可能被服务端判断为没有使用者而关闭。

用flvjs打开播放地址

POST /strm/live/open接口返回了reqId, playUrlmediaTyp之后,我们就可以创建播放器对象来播放了。 本例在调用POST /strm/live/open接口时,指定了客户端协议为HTTP FLV(proto = 1),所以服务端返回的是HTTP FLV协议的播放地址。因此,我们使用flv.js的播放器来播放。 以下是创建播放器并加载码流的代码:

    /**
* 创建播放器并加载码流
*/
function createPlayerAndLoad(url, mediaTyp) {
if (!hlsJsSupported)
return;

playingUrl = url;
let hasAudio;
let hasVideo;

// 根据`mediaTyp`设置 `hasAudio`/`hasVideo`属性
switch (mediaTyp) {
case 'av':
hasAudio = true;
hasVideo = true;
break;

case 'a':
hasAudio = true;
break;

case 'v':
hasVideo = true;
break;

default:
throw new Error(`不支持的'mediaTyp': ${mediaTyp}`);
}

const videoElement = document.getElementById('vid');

// 创建 flvPlayer
flvPlayer = flvjs.createPlayer(
{
type: 'flv',
isLive: true,
url: url,
hasAudio,
hasVideo
},
{
enableStashBuffer: false
}
);


flvPlayer.attachMediaElement(videoElement);
playRequested = true;

// 添加错误处理器
flvPlayer.on('error', (errType, detail) => {
// 发生错误时,要停止keep,停止播放器
errLog('播放时发生错误:' + errType + ', detail=' + detail);
stop(); // 关闭keep定时器,关闭播放器
})

// 在接收到码流元数据后开始播放
flvPlayer.on("metadata_arrived", () => {
if (playRequested && !playing) {
playRequested = false;
playing = true;

flvPlayer.play();
setStateText('播放中...');
}
});

// 加载码流
flvPlayer.load();

setStateText('加载中...');
flvLoaded = true;
}

结束播放

结束播放时,可先关闭播放器,停止调用保持接口,最后调用控制接口POST /strm/live/ctrl,这样整个播放过程就结束了。

    /**
* 停止播放
*/
function stop() {
// 关闭 keep-alive 定时器
cancelTimer();

// 关闭、销毁播放器
stopPlayer();

// 调用控制接口,关闭通道
apiStop();

changeStateToClosed();
}

如果没有调用POST /strm/live/ctrl来关闭通道,服务端将在保持超时后关闭通道。

同步模式的问题

本例使用同步打开模式,POST /strm/live/open接口返回成功时,终端的媒体流已经准备好、可供播放了。 故而我们用返回的playUrl去加载码流时,播放器将立即开始播放。 如果出现终端不在线,或终端应答操作失败,则接口会返回相应的错误代码。此时,我们应放弃播放,将错误信息反馈给用户。

另一种情况是,部分厂商的终端响应比较慢,某些情况下要10多秒才开始推流,这个时候,POST /strm/live/open接口将等待终端开始推流后才返回。 再一种情况是,终端响应成功的应答码,但实际因各种原因推流失败,服务端在等待一小段时间后,会重新下发指令,直到终端完成推流,或超时。以上这两种情况都使客户端在调用POST /strm/live/open接口时等待较长的时间。 如果同时打开多个终端通道,就很容易产生阻塞。

大部分浏览器都会限制在一个Tab页内同一个域名的并发连接数,以下是2020年部分浏览器的同一个域名的并发连接数限制: img_2.png

所以对于要打开多个终端通道的应用,同步模式就会不适用,这个时候,我们需要使用异步模式。接下来,我们在下一示例中看看异步模式要怎么使用。