Safari中的WebRTC教程

自Apple向Safari中加入WebRTC支持以来已经有一年多时间了,我之前关于具体实现的文章没有反映其中的一些更新。更重要的是,考虑到其中的不同和限制,对Safari来说,关于如何更好地开发WebRTC app还存在许多问题。

Cluecon上我与Chad Phillips交流,最终谈到了他在Safari上使WebRTC工作的艰难经历。对此他有许多不错的建议。

Chad经常发布一些开源代码并且也是FreeSWTICH产品的贡献者。自从2015年,他就从事WebRTC的开发工作。最近他安装了Moxiemeet,一个用来进行在线试验的视频会议平台,他是此平台的CTO,并且在本文中发表了很多想法。

000

2017年6月,Apple成为了最后一个发布WebRTC支持的主要供应商,为平台之间相互使用铺平了道路。

但是,一年之后,对于开发者来说,关于如何集成他们的WebRTC app和Safari/iOS的指导教程依然很少。除了一些由WebKit团队发布的文章,还有StackOverflow上的一些问题,我没有看到太多对此方面的支持。本文尝试填充它们之间的间隙。

我花了几个月时间来努力的使一个复杂的视频会议App在Safari中实现WebRTC.我花了很长时间来使iOS工作,尽管以下的某些阐述同样对MacOS上的Safari适用。

本文假设你具有一些实现WebRTC的相关经验,这不是一篇教初学者如何实现的文章,而是帮助有经验的开发者来细化将他们的app集成到Safari/iOS中的教程。在合适的地方我会指出Webkit bug tracker中相关的问题,这样你可以参与到对此和一些其它文章的讨论之中。

一些好消息:

首先:
1.Apple的当前实现相对不错。
2.对于1对1的视频音频电话处理,实现一体化相对简单。

让我们看看一些需求和存在的问题。

普通的教程,使人厌烦。

使用当前的WebRTC说明。

091601

如果你在通过画图来建立app,我推荐你使用目前的WebRTC API说明书,以下的资源同样不错:
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
https://github.com/webrtc/samples

如果你是在使用较老版本的WebRTC实现来运行App,我推荐你升级到最新版本,因为iOS的下一次更新会默认禁止之前的APIs.

更多的背景知识如下:
https://blog.mozilla.org/webrtc/the-evolution-of-webrtc/

iPhone和iPad具有独特的规则-测试两者

091602

由于iPhone和iPad具有不同的规则和限制,尤其是在视频方面,我强烈推荐你分别在两者上面进行测试。或许更聪明的做法是先让app在iPhone上完全工作,看起来在iPad上存在更多的限制。

更多背景知识如下:
https://webkit.org/blog/6784/new-video-policies-for-ios

让iOS的麻烦开始

可能你只需要让app在iOS上工作。如果不是的话,现在迎来了一个坏消息:iOS实现具有一些使人更狂的bug和限制,尤其是在像多方视频通话的复杂情况中。

其它浏览器在iOS上丢失WebRTC

091603

WebRTC API还没有使用WKWebView暴露给iOS浏览器。实际上,这意味着你的基于网页的WebRTC APP只能在iOS上的Safari工作,在其它用户安装好的浏览器上不能工作,例如Chrome。在Safari中的一个in-app版本也不能工作。

为此,如果用户想要通过除了Safari之外的浏览器打开app,你应该加入一些有用的错误信息。

相关的问题:
https://bugs.webkit.org/show_bug.cgi?id=183201
https://bugs.chromium.org/p/chromium/issues/detail?id=752458

无beforeunload event, 使用 pagehide

根据Safari event文件,unload event不被支持,beforeunload event在Safari中已经被完全移除。所以如果你在使用这些events,例如,为了处理清理请求,你应该在Safari上使用pagehide event。

/**
 * iOS doesn't support beforeunload, use pagehide instead.
 * NOTE: I tried doing this detection via examining the window object
 *       for onbeforeunload/onpagehide, but they both exist in iOS, even
 *       though beforeunload is never fired.
 */

var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
var eventName = iOS ? 'pagehide' : 'beforeunload';
window.addEventListener(eventName, function (event) {
  // Do the work...
});


源代码:https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe

获取,播放

 

Playsinline属性

第一步是向视频标签中添加playsinline属性,这允许视频在iOS上播放。因此,需要向video标签中添加playsinline属性。

Playsinline起初只是iOS上Safari的一个需求,但是现在某些时候你可能在Chrome中也需要使用它—查看 Dag-Inge的文章来了解更多信息。

关于此问题的更多信息如下:
https://github.com/webrtc/samples/issues/929

自动播放原则

接下来你需要了解Webkit WebRT自动播放音频视频的规则。主要规则如下:
如果网页已经抓取,MediaStream-backed 媒体将会自动播放。
如果网页正在播放音频,MediaStream-backed媒体将会自动播放
需要一个用户指令来开始任何音频重放—WebRTC或者其它。

这对通常的视频播放使用是个好消息,因为你很有可能得到了用户的许可来使用他们的麦克风或摄像头,这就满足了第一条规则。注意对于MacOS和iOS还要满足基本的自动播放规则,因此很有必要了解这些信息:

相关webkit文章:
https://webkit.org/blog/7763/a-closer-look-into-webrtc
https://webkit.org/blog/7734/auto-play-policy-changes-for-macos
https://webkit.org/blog/6784/new-video-policies-for-ios

无低视频分辨率

091604

在一个兼容WebRTC的浏览器中访问https://jsfiddle.net/thehunmonkgroup/kmgebrfz/15/ 将会使你了解到测试设备/浏览器组合通常支持的分辨率。你会注意到在MacOS和iOS的Safari上,没有任何可供选择的低视频分辨率,例如工业标准QQVGA,或160*120像素。

现在你可以发送最低的分辨率然后让接收者的浏览器来降低视频画质,但是如果下载带宽饱和,在mesh/SFU情形下将会降低网络速度。

我通过限制发送视频的比特率来解决这个问题,这是一个相对快速的折中办法。另一个解决方案是在将视频流发送到点点连接之前,处理app降低画质的视频流,尽管这会导致客户端的设备CPU消耗更大。

样例代码:
https://webrtc.github.io/samples/src/content/peerconnection/bandwidth/

新的getUserMedia()请求替代了原有的stream track

091605

如果你的app从许多getUserMedia()请求中获得视频流,在iOS中很有可能出现问题。通过我的测试,总结问题如下:如果此函数请求一个之前请求的媒体,那么之前请求的媒体的muted属性将会被设置为true,就无法在编程上unmute此属性。数据依然会通过一个peer 连接发送,但是对于muted的track的另一方来说,这就没有太大作用了。

我通过如下方法解决:

1.在app的生命周期前,提前获取一个全局的音频视频流。
2.使用MediaStream.clone() , MediaStream.addTrack() , MediaStream.removeTrack()来创建/管理额外的流,而不再调用getUserMedia().

/**
 * Illustrates how to clone and manipulate MediaStream objects.
 */

function makeAudioOnlyStreamFromExistingStream(stream) {
  var audioStream = stream.clone();
  var videoTracks = audioStream.getVideoTracks();
  for (var i = 0, len = videoTracks.length; i < len; i++) {
    audioStream.removeTrack(videoTracks[i]);
  }
  console.log('created audio only stream, original stream tracks: ', stream.getTracks());
  console.log('created audio only stream, new stream tracks: ', audioStream.getTracks());
  return audioStream;
}

function makeVideoOnlyStreamFromExistingStream(stream) {
  var videoStream = stream.clone();
  var audioTracks = videoStream.getAudioTracks();
  for (var i = 0, len = audioTracks.length; i < len; i++) {
    videoStream.removeTrack(audioTracks[i]);
  }
  console.log('created video only stream, original stream tracks: ', stream.getTracks());
  console.log('created video only stream, new stream tracks: ', videoStream.getTracks());
  return videoStream;
}
function handleSuccess(stream) {
  var audioOnlyStream = makeAudioOnlyStreamFromExistingStream(stream);
  var videoOnlyStream = makeVideoOnlyStreamFromExistingStream(stream);
  // Do stuff with all the streams...
}
function handleError(error) {
  console.error('getUserMedia() error: ', error);
}
var constraints = {
  audio: true,
  video: true,
};
navigator.mediaDevices.getUserMedia(constraints).
    then(handleSuccess).catch(handleError);


源代码: https://gist.github.com/thehunmonkgroup/2c3be48a751f6b306f473d14eaa796a0

查看这篇文章了解更多:
https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
相关问题:
https://bugs.webkit.org/show_bug.cgi?id=179363

管理媒体设备

页面重载时改变媒体设备ID

许多app都加入了用户选择音频视频设备的功能,这最终使得将deviceId传给getUserMedia()成为一个限制。
不幸的是对于开发者来说,作为Webkit的安全协议的一部分,每一次页面加载对所有的设备都会产生随机的deviceId。这意味着,不像其它平台那样,你不能仅仅保存用户的deviceId来供今后重新使用。

我是这样解决的:
1.对于用户选择的设备同时存储device.deviceId和device.label

2.对于任何将deviceId传入getUserMedia()的代码:
(1)尝试使用保存的deviceId
(2)如果失败,再次列举所有设备,尝试从保存的设备标签中寻找deviceId.

**
 * Illustrates how to handle getting the correct deviceId for
 * a user's stored preference, while accounting for Safari's
 * security protocol of serving a random deviceId per page load.
 */

// These would be pulled from some persistent storage...
var storedVideoDeviceId = '1234';
var storedVideoDeviceLabel = 'Front camera';

function getDeviceId(devices) {
  var videoDeviceId;
  // Try matching by ID first.
  for (var i = 0; i < devices.length; ++i) {
    var device = devices[i];
    console.log(device.kind + ": " + device.label + " id = " + device.deviceId);
    if (deviceInfo.kind === 'videoinput') {
      if (device.deviceId == storedVideoDeviceId) {
        videoDeviceId = device.deviceId;
        break;
      }
    }
  }
  if (!videoDeviceId) {
    // Next try matching by label.
    for (var i = 0; i < devices.length; ++i) {
      var device = devices[i];
      if (deviceInfo.kind === 'videoinput') {
        if (device.label == storedVideoDeviceLabel) {
          videoDeviceId = device.deviceId;
          break;
        }
      }
    }
    // Sensible default.
    if (!videoDeviceId) {
      videoDeviceId = devices[0].deviceId;
    }
  }
  // Now, the discovered deviceId can be used in getUserMedia() requests.
  var constraints = {
    audio: true,
    video: {
      deviceId: {
        exact: videoDeviceId,
      },
    },
  };
  navigator.mediaDevices.getUserMedia(constraints).
    then(function(stream) {
      // Do something with the stream...
    }).catch(function(error) {
      console.error('getUserMedia() error: ', error);
    });

}
function handleSuccess(stream) {
  stream.getTracks().forEach(function(track) {
    track.stop();
  });
  navigator.mediaDevices.enumerateDevices().
    then(getDeviceId).catch(function(error) {
      console.error('enumerateDevices() error: ', error);
    });
}
// Safari requires the user to grant device access before providing
// all necessary device info, so do that first.
var constraints = {
  audio: true,
  video: true,
};
navigator.mediaDevices.getUserMedia(constraints).
  then(handleSuccess).catch(function(error) {
    console.error('getUserMedia() error: ', error);
  });


源代码: https://gist.github.com/thehunmonkgroup/197983bc111677c496bbcc502daeec56

相关问题:
https://bugs.webkit.org/show_bug.cgi?id=179220

相关文章:
https://webkit.org/blog/7763/a-closer-look-into-webrtc

不支持扬声器选择

Webkit不支持HTMLMediaElement.setSinkId(), 这个API用来将音频输出到指定设备上。如果你的app加入了这个支持,你需要确保它可以处理API不被提供支持的情况。

/**
 * Illustrates methods for testing for the existence of support
 * for setting a speaker device.
 */

// Check for the setSinkId() method on HTMLMediaElement.
if (setSinkId in HTMLMediaElement.prototype) {
  // Do the work.
}

// ...or...

// Check for the sinkId property on an HTMLMediaElement instance.
if (typeof element.sinkId !== 'undefined') {
  // Do the work.
}


源代码: https://gist.github.com/thehunmonkgroup/1e687259167e3a48a55cd0f3260deb70

相关问题:
https://bugs.webkit.org/show_bug.cgi?id=179415

peer连接和通话

当心,无VP8支持

尽管W3C说明书清晰地说明实现了VP8视频编解码器,Apple选择不支持它。这是一个技术问题,就像libwebrtc只吃了VP8,但是Webkit主动禁止了它。

我的设备:
1.多方MCU—确保H.264是一个被支持的编解码器
2.多方SFU—使用H.264
3.多方Mesh和点点连接—请求所有人协商好一个通用的编解码器

对于安卓,Chrome不提供H.264支持。在我的测试当中,许多安卓手机具有H.264编码,但是那些缺失了硬件编码的手机将不能在Chrome中正常工作。
相关的错误报告:
https://bugs.webkit.org/show_bug.cgi?id=167257
https://bugs.webkit.org/show_bug.cgi?id=173141
https://bugs.chromium.org/p/chromium/issues/detail?id=719023

只发送/接收流

上文提到iOS不支持某些WebRTC API,也不是所有浏览器都支持当前的说明书。
一个好的例子就是创建一个只有发送的视频或音频连接,iOS不支持ofofferToReceiveAudio / offerToReceiveVideo的RTCPeerConnection.createOffer() 延迟选择。
并且当前版本的Chrome默认不支持RTCRtpTransceiver。

其它更多难解的bug和限制:

https://bugs.webkit.org/buglist.cgi?component=WebRTC&list_id=4034671&product=WebKit&resolution=—

原文标题:Guide to WebRTC with Safari in the Wild
作者:‘Chad Phillips’

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营