资讯专栏INFORMATION COLUMN

摆脱客户端?网页发起直播势在必行!

econi / 3655人阅读

摘要:背景近几年直播行业飞速发展,但是由于端这方面功能的长时间缺失,使得直播端以客户端为主的出现使得网页也可以成为直播端。通过发送消息到插件调起屏幕共享。的点对点连接的过程为呼叫端给接收端发送一个信息。下面简单介绍下使用声网发起直播的流程。

背景

近几年直播行业飞速发展,但是由于Web端这方面功能的长时间缺失,使得直播端以客户端为主;WebRTC 的出现使得网页也可以成为直播端。那么究竟WebRTC是什么呢?

WebRTC,即Web Real-Time Communication,web实时通信技术。简单地说就是在web浏览器里面引入实时通信,包括音视频通话等,它使得实时通信变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是通过JavaScript API即可完成;而且WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、展示等功能,还支持跨平台,包括主流的PC和移动端设备。

下面介绍下需要用到的几个API:

getUserMedia

我们可以通过调用navigator.mediaDevices.getUserMedia(constraints)去初始化一个本地的音视频流,然后把直播流通过video标签播放。代码如下:

html:

js:

const constraints = {
  audio: false,
  video: true
};

async function init(e) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    const video  = document.querySelector("video");
    video.srcObject = stream;
  } catch (e) {
    console.log(e, "stream init error");
  }
}
document.querySelector("#showVideo").addEventListener("click", (e) => init(e));

示例效果:

当然,如果有多个设备,就需要考虑设备选择和设备切换的问题。那就需要用到下面的这个API。

设备

我们看看如何用原生的Web API去获取设备(以下示例代码可适用于Chrome,其他浏览器暂未测试;具体浏览器兼容性可参考官方文档,本文档底部有链接)。

navigator.mediaDevices.enumerateDevices()

如果枚举成功将会返回一个包含MediaDeviceInfo实例的数组,它包含了可用的多媒体输入输出设备的信息。

下面是调用代码示例。

navigator.mediaDevices.enumerateDevices().then((devices) => {
  console.log(devices, "-----enumerateDevices------");
});

设备参数说明:

deviceId:设备id,具有唯一性

groupId:设备组id,不具有唯一性

kind:设备类别(audioinput:音频输入设备,audiooutput:音频输出设备,videoinput:视频输入设备)

label:设备名称(未经过授权允许的设备,label值为空,授权允许后可拿到label的值,如下两图所示)

获取的所有设备截图(未授权):

videoinput已授权截图:

获取到设备列表后,可设置navigator.mediaDevices.getUserMedia(constraints)的constraints参数选择所用设备。

const { audioList, videoList } = await getDevices();
const constraints = {
  audio: {
    deviceId: audioList[0].deviceId
  },
  video: {
    deviceId: videoList[0].deviceId
  }
};
navigator.mediaDevices.getUserMedia(constraints);

然而,我们在更换deviceId切换设备的时候发现一些异常情况。在某些deviceId之间切换时,摄像头画面或者是麦克风采集处并没有发生变化。进一步调试发现,这些切换后没有发生变化的deviceId都具有相同的groupId。因此,相同groupId下的设备,选择一个用于切换即可。

筛选麦克风、摄像头设备示例:

function getDevices() {
  return new Promise((resolve) => {
    navigator.mediaDevices.enumerateDevices().then((devices) => {
      const audioGroup = {};
      const videoGroup = {};
      const cameraList = [];
      const micList = [];
      devices.forEach((device, index) => {
        if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === "audioinput") {
          micList.push(device);
          audioGroup[device.groupId] = true;
        }

        if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === "videoinput") {
          cameraList.push(device);
          videoGroup[device.groupId] = true;
        }
      });
      resolve({ cameraList, micList });
    });
  });
}

注意:在Chrome下,电脑外接摄像头后拔出设备,此时还有可能获取到拔出的设备信息,在进行切换的时候会有问题,可以采用在页面进行友好提示处理这种情况。

屏幕共享 MediaDevices.getDisplayMedia

Chrome 72+、Firefox 66+版本已经实现了WebRTC规范中的MediaDevices.getDisplayMedia,具备屏幕共享功能。

navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false
}).then(stream => {
  video.srcObject = stream;
}).catch(err => {
  console.error(err);
});

示例效果:

对于Chrome 72以下的版本,想要实现屏幕共享的功能需要借助Chrome插件去获取screen(显示器屏幕)、application windows(应用窗口)和browser tabs(浏览器标签页)。 Chrome插件:由manifest.json和script.js组成。

manifest.json 填入一些基本数据。

background中scripts传入需执行的js文件。

添加permissions: ["desktopCapture"],用来开启屏幕共享的权限。

externally_connectable用来声明哪些应用和网页可以通过runtime.connectruntime.sendMessage连接到插件。

{
   "manifest_version": 2,
   "name": "Polyv Web Screensharing",
   "permissions": [ "desktopCapture" ],
   "version": "0.0.1",
   "background": {
      "persistent": false,
      "scripts": [ "script.js" ]
   },
   "externally_connectable": {
      "matches": ["*://localhost:*/*"]
   }
}

script.js

// script.js
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (request.getStream) {
      // Gets chrome media stream token and returns it in the response.
      chrome.desktopCapture.chooseDesktopMedia(
        ["screen", "window", "tab"], sender.tab,
        function(streamId) {
          sendResponse({ streamId: streamId });
        });
      return true; // Preserve sendResponse for future use
    }
  }
);

在页面中开始屏幕共享。通过chrome.runtime.sendMessage发送消息到Chrome插件调起屏幕共享。获取到streamId后,通过mediaDevices.getUserMedia得到stream。

const EXTENSION_ID = "";
const video = $("#videoId");
chrome.runtime.sendMessage(EXTENSION_ID, { getStream: true }, res => {
  console.log("res: ", res);
  if (res.streamId) {
    navigator.mediaDevices.getUserMedia({
      video: {
        mandatory: {
          chromeMediaSource: "desktop",
          chromeMediaSourceId: res.streamId
        }
      }
    }).then((stream) => {
      video.srcObject = stream;
      video.onloadedmetadata = function(e) {
        video.play();
      };
    })
  } else {
    // 取消选择
  }
});

而Firefox 66版本以下,不需要像Chrome借助插件才能实现屏幕共享。Firefox 33之后可以直接通过使用mediaDevices.getUserMedia,指定约束对象mediaSource为screen、window、application来实现屏幕共享。不过在Firefox中,一次只能指定一种mediaSource。

navigator.mediaDevices.getUserMedia({
  video: {
    mediaSource: "window" 
  }
}).then(stream => {
    video.srcObject = stream;
});
传输

WebRTC的RTCPeerConnection可以建立点对点连接通信,RTCDataChannel提供了数据通信的能力。

WebRTC的点对点连接的过程为:

呼叫端给接收端发送一个offer信息。在发送给接收端之前先调用setLocalDescription存储本地offer描述。

接收端收到offer消息后,先调用setRemoteDescription存储远端offer,再创建一个answer信息给呼叫端。

RTCDataChannel提供了send方法和message事件。使用起来与WebSocket类似。

由于没有服务器,以下代码为呼叫端和接收端在同一页面上,RTCPeerConnection对象之间是如何进行数据交互。

// 创建数据通道
sendChannel = localConnection.createDataChannel("通道名称", options);
sendChannel.binaryType = "arraybuffer";
  sendChannel.onopen = function() {
  sendChannel.send("Hi there!");
};
sendChannel.onmessage = function(evt) {
  console.log("send channel onmessage: ", evt.data);
};

// 远端接收实例
remoteConnection = new RTCPeerConnection(servers);
remoteConnection.onicecandidate = function(evt) {
  if (evt.candidate) {
    localConnection.addIceCandidate(new RTCIceCandidate(evt.candidate));
  }
};
// 当一个RTC数据通道已被远端调用createDataChannel()添加到连接中时触发
remoteConnection.ondatachannel = function() {
  const receiveChannel = event.channel;
  receiveChannel.binaryType = "arraybuffer";
  //接收到数据时触发
  receiveChannel.onmessage = function(evt) {
    console.log("onmessage", evt.data); // log: Hi there!
  };
  receiveChannel.send("Nice!");
};

// 监听是否有媒体流
remoteConnection.onaddstream = function(e) {
  peerVideo.srcObject = e.stream;
};

localConnection.addStream(stream);

// 创建呼叫实例
localConnection.createOffer().then(offer => {
  localConnection.setLocalDescription(offer);
  remoteConnection.setRemoteDescription(offer);
  remoteConnection.createAnswer().then(answer => {
    remoteConnection.setLocalDescription(answer);
    // 接收到answer
    localConnection.setRemoteDescription(answer);
  })
});

至此我们已经介绍完毕浏览器设备检测采集和屏幕分享的基本流程,但是光有这些可还远远不够,一套完整的直播体系包括音视频采集、处理、编码和封装、推流到服务器、服务器流分发、播放器流播放等等。如果想节省开发成本,可以使用第三方SDK。下面简单介绍下使用声网SDK发起直播的流程。

浏览器要求:

Chrome 58+

Firefox 56+

Safari 11+(屏幕共享不可用)

Opera 45+(屏幕共享不可用)

QQ 10+(屏幕共享不可用)

360 安全浏览器 9.1+(屏幕共享不可用)

设备检测

调用AgoraRTC.getDevices获取当前浏览器检测到的所有可枚举设备,kind为"videoinput"是摄像头设备,kind为"audioinput"是麦克风设备,然后通过createStream初始化一个本地的流。 获取设备:

AgoraRTC.getDevices((devices) => {
    const audioGroup = {};
    const videoGroup = {};
    const cameraList = [];
    const micList = [];
    devices.forEach((device, index) => {
        if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === "audioinput") {
            micList.push(device);
            audioGroup[device.groupId] = true;
        }

        if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === "videoinput") {
            cameraList.push(device);
            videoGroup[device.groupId] = true;
        }
    });
    return { cameraList, micList };
});

初始化本地流:

// uid:自定义频道号,cameraId设备Id
const stream = AgoraRTC.createStream({
    streamID: uid,
    audio: false,
    video: true,
    cameraId: cameraId,
    microphoneId: microphoneId
});
stream.init(() => {
    // clientCamera 
stream.play("clientCamera", { muted: true }); }, err => { console.error("AgoraRTC client init failed", err); });

stream.init()初始化直播流;如果当前浏览器摄像头权限为禁止,则调用失败,可捕获报错Media access NotAllowedError: Permission denied; 若摄像头权限为询问,浏览器默认弹窗是否允许使用摄像头,允许后调用play()可看到摄像头捕获的画面。 如果不传入cameraId,SDK会默认获取到设备的deviceId,如果权限是允许,同样会显示摄像头画面。

采集 摄像头

顺利拿到cameraId和microphoneId后就可以进行直播。通过SDK提供的createStream创建一个音视频流对象。执行init方法初始化成功之后,播放音视频(见上文)。最后通过client发布流以及推流到CDN(见下文)。

屏幕共享

Web 端屏幕共享,通过创建一个屏幕共享的流来实现的。Chrome屏幕共享需要下载插件,在创建的流的时候还需要传入插件的extensionId。

const screenStream = AgoraRTC.createStream({
    streamID: ,
    audio: false,
    video: false,
    screen: true,
    extensionId: , // Chrome 插件id
    mediaSource: "screen" // Firefox
});
传输

通过AgoraRTC.createStream创建的音视频流,通过publish发送到第三方服务商的SD-RTN(软件定义实时传输网络)。

client.publish(screenStream, err => {
  console.error(err);
});

别的浏览器可以通过监听到stream-added事件,通过subscribe订阅远端音视频流。

client.on("stream-added", evt => {
  const stream = evt.stream;
  client.subscribe(stream, err => {
    console.error(err);
  });
});

再通过startLiveStreaming推流到CDN。

// 编码
client.setLiveTranscoding();
client.startLiveStreaming(, true)

在推摄像头流的时候,关闭摄像头,需要推一张占位图。这个时候先用canvas画图,然后用WebRTC提供的captureStream捕获静态帧。再调用getVideoTracks,制定AgoraRTC.createStream的videoSource为该值。视频源如来自 canvas,需要在 canvas 内容不变时,每隔 1 秒重新绘制 canvas 内容,以保持视频流的正常发布。

const canvas = document.createElement("canvas");
renderCanvas(canvas);
setInterval(() => {
  renderCanvas(canvas);
}, 1000);
canvasStream = canvas.captureStream();

const picStream = AgoraRTC.createStream({
  streamID: ,
  video: true,
  audio: false,
  videoSource: canvasStream.getVideoTracks()[0]
});

// 画图
function renderCanvas(canvas) {
  ...
}

一个client只能推一个流,所以在进行屏幕共享的时候,需要创建两个client,一个发送屏幕共享流,一个发送视频流。屏幕共享流的video字段设为false。视频流的video字段设为true。然后先通过setLiveTranscoding合图再推流。

const users = [
  {
    x: 0, // 视频帧左上角的横轴位置,默认为0
    y: 0, // 视频帧左上角的纵轴位置,默认为0
    width: 1280, // 视频帧宽度,默认为640
    height: 720, // 视频帧高度,默认为360
    zOrder: 0, // 视频帧所处层数;取值范围为 [0,100];默认值为 0,表示该区域图像位于最下层
    alpha: 1.0, // 视频帧的透明度,默认值为 1.0
    uid: 888888, // 旁路推流的用户 ID
  },
  {
    x: 0,
    y: 0,
    width: 1280,
    height: 720,
    zOrder: 1,
    alpha: 1.0,
    uid: 999999
  }
];
    
var liveTranscoding = {
  width: 640,
  height: 360,
  videoBitrate: 400,
  videoFramerate: 15,
  lowLatency: false,
  audioSampleRate: AgoraRTC.AUDIO_SAMPLE_RATE_48000,
  audioBitrate: 48,
  audioChannels: 1,
  videoGop: 30,
  videoCodecProfile: AgoraRTC.VIDEO_CODEC_PROFILE_HIGH,
  userCount: user.length,
  backgroundColor: 0x000000,
  transcodingUsers: users,
};
client.setLiveTranscoding(liveTranscoding);

因为业务需求是摄像头和屏幕共享可以切换,摄像头和屏幕共享的分辨率和码率均不相同,屏幕共享需要更高的分辨率和码率。但是开发中发现切换时设置码率无效。SDK那边给的答复是:因为缓存问题,会以第一次推流设置的参数为准,将会在下个版本中修复。

参考文献:
MediaDevices.getUserMedia()
MedaiDevices.enumerateDevices()
HTMLMediaElement
MediaDevices/getDisplayMedia

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104674.html

相关文章

  • 低代码:当今最新的流行语还是名归实至?

    摘要:低代码技术近期受到了广泛的关注。谈到低代码,答案有两部分当今组织敏捷性势在必行敏捷和快速有效地响应内外部变化的能力通常是现代企业成功最关的键因素。 低代码技术近期受到了广泛的关注。即使是领先的分析公司也对这一趋势表达了自己的看法;事实上,Forrester预测低代码收入增长将超过68%,到2020年整体市场规模达到155亿美元。 那么为什么低代码解决方案最近受到如此多的关注呢? 它有保...

    mengera88 评论0 收藏0
  • 保利威无延迟直播:全面增强互动体验,大班课、带货直播转化更高效!

    摘要:保利威无延迟直播可以在手机电脑上实现高参与度的互动,让课堂体验再上一层,对于提升公开课转化率非常有帮助。无延迟体验能让直播更接近线下体验。     衡量一场直播是否成功,用户互动体验必然是关键一环。   今年疫情影响下,云办公、云上课、云会展、云购物纷纷兴起。带货直播、空中课堂、会展直播等多样化的场景让用户对直播实时性、流畅性有了更高要求。   ...

    不知名网友 评论0 收藏0
  • UCloud+保利威=?Polyv云直播U享版PLive“出世“!提供2000分钟云直播免费测试额度

    摘要:视频云联合大促活动对象新老用户均可参加,各规则限购次。云直播特惠套餐无需开发提供超项功能全终端低延迟开箱即用的直播服务。功能完善超项直播功能,可实现企业各类直播的互动营销定制大数据运营等需求。 UCloud+保利威=?UCloud最近新上线了【CDN&视频云联合大促】活动:CDN低至0.01元/GB 9.9元抢实时音视频10万分钟时长包,除了CDN流量包、URTC实时音视频时长包和云直播U...

    zollero 评论0 收藏0

发表评论

0条评论

econi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<