尽管Apple在2017年的WWDC上宣布加入WebRTC支持,但仍然没有看到Apple在支持WebRTC上更深入的举动,尤其是其不只支持VP8更加强了这种担忧。
文 / Chad Phillips
译 / 元宝
原文:https://webrtchacks.com/guide-to-safari-webrtc/
自Apple首次向Safari添加WebRTC支持以来,已有一年多的时间了。鉴于WebRTC的差异和局限性,如何最好地开发Safari的WebRTC应用程序仍然存在许多问题。Chad是长期开源人员,也是FreeSWITCH产品的贡献者。他自2015年以来一直参与WebRTC的开发工作。他最近推出了MoxieMeet,一个在线体验活动的视频会议平台,在那里他担任首席技术官,并为这篇文章将展示他许多见解。
Safari和WebRTC在野外。由Flickr用户Curious Expeditions(CC BY-NC-SA 2.0)添加到Mountain Lion攻击鹿 - 动物标本的照片
在2017年6月,苹果成为最后一个发布WebRTC支持的主要供应商,为平台的互操作性铺平了(仍然坎坷的)道路。
然而,一年多以后,我对开发人员仍然缺乏可用于将WebRTC应用程序与Safari / iOS集成的指南感到惊讶。除了Webkit团队的一些帖子之外,还有一些分散的StackOverflow问题,从WebRTC的Webkit bug报告中收集到的知识,以及这些网站上得的一些帖子,我真的没有看到很多可用的支持。这篇文章试图开始纠正这一差距。
我花了很多个月的努力将WebRTC集成到Safari中,用于非常复杂的视频会议应用程序。我的大部分时间花在了iOS工作上,尽管下面的一些指针也适用于MacOS上的Safari。
这篇文章假设您在实施WebRTC方面有一定的经验——这并不是初学者的方法,而是有经验的开发人员指导他们平滑的将他们的应用程序与Safari / iOS集成的过程。在适当的情况下,我将指出Webkit bug跟踪器中提交的相关问题,以便您可以将您的声音添加到这些讨论中,以及其他一些信息丰富的帖子中。
为了在我的应用程序中声明iOS支持,我做了大量探索,希望下面的知识将使您的旅程更加顺畅!
首先是一些好消息
第一,好消息是:
苹果目前的实施相当稳固
对于简单的1-1音频/视频通话,集成非常简单
让我们来看看一些需求和问题所在。
一般准则和烦恼
使用当前的WebRTC规范
如果您是从头开始构建应用程序,我建议使用当前的WebRTC API规范(它经历了几次迭代)。以下资源在这方面很棒:
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
https://github.com/webrtc/samples
对于那些运行具有较旧WebRTC实施的应用程序的人,我建议您尽可能升级到最新规范,因为iOS的下一个版本默认禁用旧版API。特别是,最好避免使用传统的addStream API,这使得操作流中的轨道变得更加困难。
有关此问题的更多背景信息:https://blog.mozilla.org/webrtc/the-evolution-of-webrtc/
iPhone和iPad有独特的规则 - 测试两者
由于iPhone和iPad有不同的规则和限制,特别是在视频方面,我强烈建议您在两台设备上测试您的应用程序。从iPhone开始全面工作可能更聪明,这似乎比iPad有更多限制。
更多背景信息:https://webkit.org/blog/6784/new-video-policies-for-ios
让iOS疯狂开始吧
您可能只需要将应用程序在iOS上运行即可。如果没有,现在就出现了坏消息:iOS实现有一些相当令人抓狂的错误/限制,特别是在多方会议电话等更复杂的情况下。
iOS上的其他浏览器缺少WebRTC集成
WebRTC API尚未向使用WKWebView的IOS浏览器公开。实际上,这意味着您的基于Web的WebRTC应用程序仅适用于iOS上的Safari,而不适用于用户可能安装的任何其他浏览器(例如Chrome),也不适用于Safari的“应用程序内”版本。
为避免用户混淆,如果他们尝试在除Safari之外的其他浏览器/环境中打开您的应用,您可能希望包含一些有用的用户错误消息。
相关问题:
https://bugs.webkit.org/show_bug.cgi?id=183201
https://bugs.chromium.org/p/chromium/issues/detail?id=752458
没有beforeunload事件,请使用pagehide
根据这个Safari事件文档,不推荐使用“unload”事件,并且已在Safari中完全删除了 “beforeunload”事件。因此,如果您正在使用这些事件,例如,为了处理调用清理,您将需要重构代码,以在Safari上使用 “pagehide”事件。
/**
* 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 id="video-tag" autoplay></video>
变成这样:
<video id="video-tag" autoplay playsinline></video>
“playsinline”最初只是iOS上Safari的一项要求,但现在你可能需要在某些情况下在Chrome中使用它 - 请参阅https://github.com/webrtc/samples/issues/929
自动播放规则
接下来,您需要了解有关自动播放音频/视频的Webkit WebRTC规则。主要规则是:
如果网页已经捕获,MediaStream支持的媒体将自动播放。
如果网页已播放音频,MediaStream支持的媒体将自动播放
需要用户手势来启动任何音频回放 - 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
没有低/有限的视频分辨率
测试常见的视频分辨率和Safari / iOS中的结果
在WebRTC兼容的浏览器中访问https://jsfiddle.net/thehunmonkgroup/kmgebrfz/15/(或webrtcHack的WebRTC-Camera-Resolution项目),可以快速分析测试设备/浏览器支持的常用分辨率组合。您会注意到在MacOS和iOS上的Safari中,没有任何可用的低视频分辨率,例如行业标准QQVGA或160×120像素。这些小分辨率对于提供缩略图大小的视频非常有用 - 例如,想想Google Hangouts调用中的用户幻灯片。
现在,您可以发送对等连接中最低可用原始分辨率的任何内容,并让接收器的浏览器缩小视频,但是对于在网格/ SFU场景中具有较低速度的互联网的用户,您将面临使下载带宽饱和的风险。
我通过限制发送视频的比特率来解决这个问题,这是一个相当快速和低端的妥协办法。另一个需要更多工作的解决方案是在将应用程序中的视频流传递给对等连接之前对其进行缩减,尽管这会导致客户端的设备花费一些CPU周期。
示例代码:
https://webrtc.github.io/samples/src/content/peerconnection/bandwidth/
新的getUserMedia()请求会终止现有的流跟踪
Apple的WebRTC实现仅允许一次捕获一个getUserMedia
如果您的应用程序从多个“getUserMedia()”请求中获取媒体流,则可能会出现iOS问题。从我的测试中,这个问题可以总结如下:如果“getUserMedia()”请求在先前请求的媒体类型“getUserMedia()”,先前请求媒体轨道的“静音” 属性设置为true,并没有以编程方式取消静音。数据仍然会通过对等连接发送,但对于轨道静音的另一方来说没什么用处!此限制是iOS上当前预期的行为。
我能够通过以下方式成功解决它:
在我的应用程序生命周期的早期抓取全局音频/视频流
使用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在页面重新加载时更改
许多应用程序包括支持用户选择音频/视频设备。这最终归结为将“deviceId”作为约束传递给“getUserMedia()”。
不幸的是,作为开发人员,作为Webkit安全协议的一部分,在每个新页面加载时为所有设备生成随机“deviceId”。这意味着,与其他平台不同,您不能简单地将用户选定的“deviceId”填充到持久存储中以供将来重用。
我发现这个问题的最简洁的解决方法是:
存放两个设备“deviceId” 和设备。 用户选择的设备的标签
对于最终将“deviceId”传递给“getUserMedia()”的任何代码工作流:
尝试使用保存的“deviceId”
如果失败,请再次枚举设备,并尝试 从保存的设备标签中查找“deviceId”。
相关说明:Webkit通过仅在用户授予设备访问权限后公开用户的实际可用设备来进一步防止指纹识别。实际上,这意味着您需要在 调用“enumerateDevices()”之前进行 “getUserMedia()” 调用 。
**
* 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方法。如果您的应用程序包含对此的支持,则需要确保它可以处理缺少基础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
PeerConnections和Calling
当心,没有VP8支持
虽然W3C规范明确规定要实施对VP8视频编解码器(以及H.264编解码器)的支持,但苹果迄今为止选择不支持它。遗憾的是,这不是技术问题,因为libwebrtc包含VP8支持,而Webkit主动禁用它。
所以在这个时候,我在各种场景中实现最佳互操作性的建议是:
多方MCU - 确保H.264是受支持的编解码器
多方SFU - 使用H.264
多方网格和点对点 - 祈祷每个人都可以协商一个共同的编解码器
我说最好的互操作,因为虽然这会让你走很远的路,但它不会一帆风顺。例如,Chrome for Android尚不支持软件H.264编码。在我的测试中,许多(但不是全部)Android手机都采用硬件H.264编码,但那些缺少硬件编码的手机在Chrome中不能用于Android。
相关错误报告:
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不支持旧版 RTCPeerConnection.createOffer()选项offerToReceiveAudio /offerToReceiveVideo,以及当前稳定Chrome不支持RTCRtpTransceiver 默认规格。
其他更深奥的错误和限制
当然,你可以点击的其他一些极端案例似乎有点超出了这篇文章的范围。但是,一个优秀的资源应该是Webkit问题队列,你可以只针对与WebRTC相关的问题进行过滤:
https://bugs.webkit.org/buglist.cgi?component = WebRTC&list_id = 4034671&product = WebKit&resolution = -
请记住,Webkit / Apple的实现还很年轻
它仍然缺少一些功能(如上面提到的扬声器选择),而且在我的测试中,它的稳定性不如GoogleChrome中更成熟的实现。
还有一些主要的错误- 捕获音频在iOS 12 Beta发布周期的大部分时间内完全被破坏(谢天谢地,他们最终修复了Beta 8)。
苹果对WebRTC作为平台的长期承诺尚不清楚,特别是因为除了基本支持之外,他们还没有发布有关它的更多信息。例如,前面提到的缺乏VP8的支持对于他们遵守W3C规范的意图是令人不安的。
在考虑浏览器原生实现与本地应用程序时,这些是值得考虑的事情。目前,我持谨慎乐观的态度,并希望他们对WebRTC的支持将继续下去,并扩展到iOS上的其他非Safari浏览器。