尽管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浏览器。

Safari上使用WebRTC指南相关推荐

  1. 在win10上编译webRTC(问题篇)

    参考链接:https://webrtc.org.cn/mirror/ 主要是记录<在win10上编译webRTC(编译篇)>中,遇到的问题,以及解决方案.仅作为记录用. 问题一 在编译生成 ...

  2. ios html5 录音功能,HTML5 Audio 在 iOS Safari 上的有关问题

    HTML5 Audio 在 iOS Safari 上的问题 最近接触一个移动短项目,做摇一摇的功能,然后摇的时候要有声音,摇中奖的时候也有声音,问题来了,iOS 5 不能用代码去触发播放声音,其实 A ...

  3. Safari即将支持WebRTC

    自从开始做WebRTC开发以来,经常被别人问到,safari浏览器能支持WebRTC吗?我也很希望safari能支持WebRTC,这样就不用写原生WebRTC应用或者Safari浏览器插件了. 很高兴 ...

  4. jQuery的animate()的scrollTop属性在iPad Safari上不起作用

    问题:jQuery的animate()的scrollTop属性在iPad Safari上不起作用 方案: // 在Safari上不起作用,其他浏览器可以 $("html").ani ...

  5. 掌上游戏机开发指南GBA探索日记(9)(转)

    掌上游戏机开发指南GBA探索日记(9)(转)[@more@] 这一节我们讨论如何正常结束声音播放的问题.不要觉得这是个小问题,当初这个小问题难倒了我们所有研究GBA的爱好者.我总觉得这一点似乎是GBA ...

  6. 阿里云上搭建webRTC 服务器——Licode

    阿里云上搭建webRTC 服务器--Licode 系统配置 阿里云服务器 Ubuntu 14.04.5 LTS Docker 环境搭建 在一台空的机器上搭建docker环境,先要安装docker,执行 ...

  7. 在“小程序”PWA上开发WebRTC

    严格的说,PWA与微信小程序不同,前者更加开放,功能比Web更强(接近原生应用),而微信小程序更封闭,是Web的子集.随着Mozilla.微软和苹果陆续在PWA上投入,当然还有Google不遗余力的推 ...

  8. C#在Linux上的开发指南

    本人才疏学浅,在此记录自己用C#在Linux上开发的一点经验,写下这篇指南.(给想要在Linux上开发C#程序的朋友提供建议) 目前在Linux上跑的网站:http://douxiubar.com | ...

  9. GitHub上Swift语言指南

    Swift 语言指南 @SwiftLanguage 更新于 2016-6-6,更新内容详见 Issue 55.往期更新回顾详见<收录周报> 这份指南汇集了 Swift 语言主流学习资源,并 ...

最新文章

  1. 回归算法 - 线性回归求解 θ(最大似然估计求解)
  2. 1024 程序员节专题论坛来袭,权威解读 MindSpore、CANN 5.0 特性和 HCIA MDC 开发者认证...
  3. GDI+:自定义控件时如何使用Region来输出特定区域
  4. Siamese Neural Networks for One-shot Image Recognition
  5. By.css 的级联读取
  6. 每天10分钟用python学数据分析_用Python做数据分析,Numpy,Pandas,matp
  7. linx vim 文件操作 ubuntu server 软件源
  8. 15个示例让你搞懂Linux中的cd命令
  9. php报错处理,关于升级php7后的报错处理
  10. [转载] python中try Except抛出异常使用方法
  11. Zemax操作--2(单透镜和双胶合透镜优化)
  12. 迪尼斯神奇英语全32集含教材
  13. ESP32-WROOM-32E,WIFI基本功能实现,采坑经验
  14. 去哪下载python项目_Python 项目实践二(下载数据)第三篇
  15. Nestjs模块机制的概念和实现原理
  16. MERGE操作学习总结
  17. 3698: XWW的难题 有源汇上下界最大流
  18. matlab 距平,MATLAB及其在地学中地应用.PDF
  19. IBM Cloud Computing Practitioners 2019 (IBM云计算从业者2019)Exam答案
  20. 算法题/青蛙跳台阶问题

热门文章

  1. Codeforces Codeforces Round #319 (Div. 2) A. Multiplication Table 水题
  2. 小型校园网络拓扑RS配置
  3. postfix+squirrelmail - rhat 5.4
  4. YY提交招股书赴美上市:连续三季盈利(转)
  5. CodeForces - 571D Campus(数据结构综合)
  6. java如果属性为空返回其他_后台返回前台数据(实体类)如果存在为空或‘’的属性,如何过滤掉...
  7. TensorFlow2-迁移学习
  8. jbpm springboot mysql_SpringBoot开发案例之整合Activiti工作流引擎
  9. 破解SQLSERVER存储过程的加密
  10. MFC拖拽文件到任意EDIT控件