摘自: https://www.html5rocks.com/zh/tutorials/getusermedia/intro/
<
iframe src=\”https://www.html5rocks.com/zh/tutorials/getusermedia/intro/\” class=\”iframe-full-content\”>
HTML5 Rocks
Capturing Audio & Video in HTML5
This article discusses APIs that are not yet fully standardized
and still in flux. Be cautious when using experimental APIs in your own projects.
简介
长久以来,音频/视频捕获都是网络开发中的“圣杯”。多年来,我们总是依赖于浏览器插件(Flash 或 Silverlight)实现这一点。快来看看吧!
现在轮到 HTML5 大显身手了。也许看起来不是很显眼,但是 HTML5 的崛起引发了对设备硬件访问的激增。地理位置 (GPS)、Orientation API(加速计)、WebGL (GPU) 和 Web Audio API(视频硬件)都是很好的例子。这些功能非常强大,展示了基于系统底层硬件功能之上的高级 JavaScript API。
本教程介绍了一种新 API:navigator.getUserMedia()
,可让网络应用访问用户的相机和麦克风。
getUserMedia() 的历史
如果您还不知道,getUserMedia()
的历史可谓一段有趣的故事。
过去几年中出现过好几种“Media Capture API”的变体。很多人意识到,需要能够在网络上访问本地设备,但这要所有人合力开发出一种新的规范。局面一片混乱,以至于 W3C 最终决定成立一个工作组。他们只有一个目的:理清混乱的局面!设备 API 政策 (DAP) 工作组负责对过剩的提议进行统一和标准化。
我会试着总结一下 2011 所发生的事情…
第 1 轮:HTML 媒体捕获
HTML 媒体捕获是 DAP 在网络媒体捕获标准化上迈出的第一步。具体方法是超载 并为
accept
参数添加新值。
如果您要让用户通过网络摄像头拍摄自己的快照,就可以使用 capture=camera
:
录制视频或音频也是类似的:
挺不错吧?它可以重复使用文件输入,这点我特别喜欢。这在语义上非常有意义。这种特定“API”的不足之处在于,无法处理即时效果(例如将实时网络摄像头数据呈现到 并应用 WebGL 过滤器)。HTML 媒体捕获只能让您录制媒体文件或及时拍摄快照。
支持:
- Android 3.0 浏览器 – 首次实施的一个例子。请观看此视频,了解其实际使用情况。
- Android 版 Chrome 浏览器 (0.16)
除非您使用的是以上某个移动浏览器,否则我建议您不要使用该 API。供应商纷纷转向 getUserMedia()
。其他任何人都不太可能会长期实施 HTML 媒体捕获。
第 2 轮:设备元素
很多人认为 HTML 媒体捕获的局限性太大,因此一种新的规范应运而生,可以支持任何类型的(未来)设备。不出意料地,该设计需要新的 元素,也就是
getUserMedia()
的前身。
Opera 是第一批根据 元素创建视频捕获的初始实施的浏览器之一。不久之后(准确地说是同一天),WhatWG 决定废止
标记,以支持称为
navigator.getUserMedia()
的新兴 JavaScript API。一周后,Opera 推出的新版本中加入了对更新的 getUserMedia()
规范的支持。当年年底,Microsoft 也加入这一行列,发布了 IE9 实验室以支持新规范。
的效果如下:
function update(stream) {
document.querySelector(\'video\').src = stream.url;
}
支持:
很遗憾,已发布的浏览器中没有任何一款曾经包含 。我猜这是一个不太需要担心的 API。但是
确实有两大优点:一是语义方面,二是可以轻松进行扩展,而不仅仅是支持音频/视频设备。
现在深吸一口气。这玩意儿速度飞快!
第 3 轮:WebRTC
元素最终还是像渡渡鸟一样销声匿迹了。
依靠 WebRTC(网络即时通信)的大力协助,最近几个月寻找合适捕获 API 的步伐加快了很多。该规范由 W3C WebRTC 工作组负责监管。Google、Opera、Mozilla 和其他一些公司目前正致力于在自己的浏览器中实施该 API。
getUserMedia()
与 WebRTC 相关,因为它是通向这组 API 的门户。它提供了访问用户本地相机/麦克风媒体流的手段。
支持:
在 Chrome 浏览器 18.0.1008 和更高版本中,可在 about:flags
下启用 WebRTC。
使用入门
利用 navigator.getUserMedia()
,我们最终实现了在没有插件的情况下访问网络摄像头和麦克风输入内容。相机访问权限现在和调用有关,而不是和安装有关。它直接内嵌在浏览器中。感到兴奋了吗?
启用
getUserMedia()
API 还很新,只有 Google 和 Opera 在开发人员版本中加入了它。在 Chrome 18 和更高版本中,可通过访问 about:flags
启用该 API。

about:flags
页中启用 getUserMedia()
。对于 Opera,请下载某个实验性 Android 和桌面计算机版本。
功能检测
功能检测是简单地检查是否存在 navigator.getUserMedia
:
function hasGetUserMedia() {
// Note: Opera builds are unprefixed.
return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia);
}
if (hasGetUserMedia()) {
// Good to go!
} else {
alert(\'getUserMedia() is not supported in your browser\');
}
获取输入设备的访问权限:
要使用网络摄像头或麦克风,我们需要请求权限。getUserMedia()
的第一个参数用于指定您要访问的媒体类型。例如,如果您要请求访问网络摄像头,第一个参数就应该是 \"video\"
。要同时使用麦克风和相机,则传递 \"video, audio\"
:
var onFailSoHard = function(e) {
console.log(\'Reeeejected!\', e);
};
// Not showing vendor prefixes.
navigator.getUserMedia(\'video, audio\', function(localMediaStream) {
var video = document.querySelector(\'video\');
video.src = window.URL.createObjectURL(localMediaStream);
// Note: onloadedmetadata doesn\'t fire in Chrome when using it with getUserMedia.
// See crbug.com/110938.
video.onloadedmetadata = function(e) {
// Ready to go. Do some stuff.
};
}, onFailSoHard);
好吧,这到底是怎么一回事呢?媒体捕获是各种新 HTML5 API 进行协作的绝佳示例。参与协作的还有其他一些 HTML 元素,例如 和
。请注意,我们不是要设置
src
属性或在 元素中加入
元素。我们不会向视频馈入媒体文件的网址,而是馈入从代表网络摄像头的
LocalMediaStream
对象获得的 Blob 网址。
我还会将 设置为
autoplay
,否则它会停在第一帧。添加 controls
也能达到您预期的效果。
请注意:在 Chrome 浏览器中存在一个错误,导致仅仅传递“audio”无效:crbug.com/112367。我也无法在 Opera 中正常使用 。
Opera 和 Chrome 浏览器实施的是该规范的不同版本。这导致实际使用起来要比预期的更有“挑战性”。
在 Chrome 浏览器中:
该代码段适用于 Chrome 18 和更高版本(在 about:flags
中启用):
navigator.webkitGetUserMedia(\'audio, video\', function(localMediaStream) {
var video = document.querySelector(\'video\');
video.src = window.webkitURL.createObjectURL(localMediaStream);
}, onFailSoHard);
在 Opera 中:
Opera 开发人员版本不支持该规范的更新版本。该代码段适用于 Opera:
navigator.getUserMedia({audio: true, video: true}, function(localMediaStream) {
video.src = localMediaStream;
}, onFailSoHard);
关键的区别之处在于:
-
getUserMedia()
是无前缀的。 - 对象作为第一个参数而不是字符串列表进行传递。
- 将
video.src
直接设置为LocalMediaStream
对象,而不是 Blob 网址。据我所知,Opera 最终会更新此设置,改为要求 Blob 网址。
对于这两者:
如果您希望能跨浏览器通用(但是这样很容易出问题),请尝试如下方法:
var video = document.querySelector(\'video\');
if (navigator.getUserMedia) {
navigator.getUserMedia({audio: true, video: true}, function(stream) {
video.src = stream;
}, onFailSoHard);
} else if (navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(\'audio, video\', function(stream) {
video.src = window.webkitURL.createObjectURL(stream);
}, onFailSoHard);
} else {
video.src = \'somevideo.webm\'; // fallback.
}
请务必查看 Mike Taylor 和 Mike Robinson 的 gUM Shield。它可以很好地将各浏览器实施之间的不一致“标准化”。
安全
将来,浏览器在调用 getUserMedia()
时可能会弹出信息栏,让用户选择授予还是拒绝对其相机/麦克风的访问权限。很遗憾,该规范在安全方面非常薄弱。目前,没有任何浏览器实施了权限栏。
提供回退
对于无法获得 getUserMedia()
支持的用户,如果 API 不受支持且/或由于某些原因而调用失败,可以选择回退到现有的视频文件:
// Not showing vendor prefixes or code that works cross-browser:
function fallback(e) {
video.src = \'fallbackvideo.webm\';
}
function success(stream) {
video.src = window.URL.createObjectURL(stream);
}
if (!navigator.getUserMedia) {
fallback();
} else {
navigator.getUserMedia({video: true}, success, fallback);
}
基本演示
截取屏幕截图:
API 的
ctx.drawImage(video, 0, 0)
方法可以轻松地将 帧绘制到
上。当然,既然我们通过
getUserMedia()
获得了视频输入,就可轻松地使用即时视频创建照相亭应用了。
var video = document.querySelector(\'video\');
var canvas = document.querySelector(\'canvas\');
var ctx = canvas.getContext(\'2d\');
var localMediaStream = null;
function snapshot() {
if (localMediaStream) {
ctx.drawImage(video, 0, 0);
// \"image/webp\" works in Chrome 18. In other browsers, this will fall back to image/png.
document.querySelector(\'img\').src = canvas.toDataURL(\'image/webp\');
}
}
video.addEventListener(\'click\', snapshot, false);
// Not showing vendor prefixes or code that works cross-browser.
navigator.getUserMedia({video: true}, function(stream) {
video.src = window.URL.createObjectURL(stream);
localMediaStream = stream;
}, onFailSoHard);
应用效果
CSS 过滤器
目前,WebKit Nightlies 版以及 Chrome 浏览器 18 和更高版本支持 CSS 过滤器。
使用 CSS 过滤器,我们可以在捕获 时应用一些很棒的效果:
video {
width: 307px;
height: 250px;
background: rgba(255,255,255,0.5);
border: 1px solid #ccc;
}
.grayscale {
+filter: grayscale(1);
}
.sepia {
+filter: sepia(1);
}
.blur {
+filter: blur(3px);
}
...
var idx = 0;
var filters = [\'grayscale\', \'sepia\', \'blur\', \'brightness\', \'contrast\', \'hue-rotate\',
\'hue-rotate2\', \'hue-rotate3\', \'saturate\', \'invert\', \'\'];
function changeFilter(e) {
var el = e.target;
el.className = \'\';
var effect = filters[idx++ % filters.length]; // loop through filters.
if (effect) {
el.classList.add(effect);
}
}
document.querySelector(\'video\').addEventListener(\'click\', changeFilter, false);
WebGL 纹理
视频捕获的一个精彩用例就是以 WebGL 纹理的形式呈现实时输入。由于我对 WebGL 一无所知(除了知道它很好),我建议您看看杰罗姆·艾蒂安 (Jerome Etienne) 的教程和演示。其中介绍了如何使用 getUserMedia()
和 Three.js 将直播视频呈现到 WebGL 中。
通过 Web Audio API 使用 getUserMedia
这部分介绍了当前 API 可能在未来做出的改进和扩展。
我有一个梦想,就是只通过开放网络技术在浏览器中构建 AutoTune!这个梦想很快就要实现了。对于麦克风输入,我们已经有了 getUserMedia()
。通过 Web Audio API 加入即时效果,我们就大功告成了。将以上两者结合就完成了拼图的最后一块 (crbug.com/112404),但是在工作中还要实现一个初步提议。
有朝一日,将麦克风输入输送到 Web Audio API 可能会是这样的:
var context = new window.webkitAudioContext();
navigator.webkitGetUserMedia({audio: true}, function(stream) {
var microphone = context.createMediaStreamSource(stream);
var filter = context.createBiquadFilter();
// microphone -> filter -> destination.
microphone.connect(filter);
filter.connect(context.destination);
}, onFailSoHard);
如果您希望将 getUserMedia()
与 Web Audio API 结合在一起,请访问 crbug.com/112404。
总结
总体而言,网络上的设备访问向来是一大难题。很多人曾经尝试过,但是没什么人取得成功。大多数早期的思路从未在专有环境之外占据主导地位,也从未广泛采用过。
真正的问题在于,网络的安全模式与本地系统有天壤之别。例如,我可能不希望随便什么网站都有权访问我的摄像机,但是这个问题很难解决。
PhoneGap 等桥接框架有助于突破这方面的限制,但这种临时性的解决方案对于深层的根本问题而言还远远不够。要让网络应用具备与桌面计算机应用一较高下的实力,我们需要能够访问本地设备。
getUserMedia()
仅仅是对新设备类型的第一波访问。我希望在不久的将来能看到更多。
其他资源
- W3C 规范
- 布鲁斯·劳森 (Bruce Lawson) 的 HTML5Doctor 文章
- 布鲁斯·劳森的 dev.opera.com 文章
演示
- 实时照相亭
- 保罗·尼夫 (Paul Neave) 的 WebGL 相机效果
- Snapster
- WebGL 中的直播视频
function onFailSoHard(e) {
alert(\’getUserMedia() not supported in your browser.\’);
e.target.src = \’http://www.html5rocks.com/en/tutorials/video/basics/Chrome_ImF.ogv\’;
}
(function() {
var video = document.querySelector(\’#basic-stream\’);
var button = document.querySelector(\’#capture-button\’);
var localMediaStream = null;
button.addEventListener(\’click\’, function(e) {
if (navigator.getUserMedia) {
navigator.getUserMedia(\’video\’, function(stream) {
video.src = stream;
video.controls = true;
localMediaStream = stream;
}, onFailSoHard);
} else if (navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(\’video\’, function(stream) {
video.src = window.webkitURL.createObjectURL(stream);
video.controls = true;
localMediaStream = stream;
}, onFailSoHard);
} else {
onFailSoHard({target: video});
}
}, false);
document.querySelector(\’#stop-button\’).addEventListener(\’click\’, function(e) {
video.pause();
localMediaStream.stop(); // Doesn\’t do anything in Chrome.
}, false);
})();
(function() {
var video = document.querySelector(\’#screenshot-stream\’);
var button = document.querySelector(\’#screenshot-button\’);
var canvas = document.querySelector(\’#screenshot-canvas\’);
var img = document.querySelector(\’#screenshot\’);
var ctx = canvas.getContext(\’2d\’);
var localMediaStream = null;
function sizeCanvas() {
// video.onloadedmetadata not firing in Chrome. See crbug.com/110938.
setTimeout(function() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
img.height = video.videoHeight;
img.width = video.videoWidth;
}, 50);
}
function snapshot() {
ctx.drawImage(video, 0, 0);
img.src = canvas.toDataURL(\’image/webp\’);
}
button.addEventListener(\’click\’, function(e) {
if (localMediaStream) {
snapshot();
return;
}
if (navigator.getUserMedia) {
navigator.getUserMedia(\’video\’, function(stream) {
video.src = stream;
localMediaStream = stream;
sizeCanvas();
button.textContent = \’Take Shot\’;
}, onFailSoHard);
} else if (navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(\’video\’, function(stream) {
video.src = window.webkitURL.createObjectURL(stream);
localMediaStream = stream;
sizeCanvas();
button.textContent = \’Take Shot\’;
}, onFailSoHard);
} else {
onFailSoHard({target: video});
}
}, false);
video.addEventListener(\’click\’, snapshot, false);
document.querySelector(\’#screenshot-stop-button\’).addEventListener(\’click\’, function(e) {
video.pause();
localMediaStream.stop(); // Doesn\’t do anything in Chrome.
}, false);
})();
(function() {
var video = document.querySelector(\’#cssfilters-stream\’);
var button = document.querySelector(\’#capture-button2\’);
var localMediaStream = null;
var idx = 0;
var filters = [
\’grayscale\’,
\’sepia\’,
\’blur\’,
\’brightness\’,
\’contrast\’,
\’hue-rotate\’, \’hue-rotate2\’, \’hue-rotate3\’,
\’saturate\’,
\’invert\’,
\’\’
];
function changeFilter(e) {
var el = e.target;
el.className = \’\’;
var effect = filters[idx++ % filters.length];
if (effect) {
el.classList.add(effect);
}
}
button.addEventListener(\’click\’, function(e) {
if (navigator.getUserMedia) {
navigator.getUserMedia(\’video, audio\’, function(stream) {
video.src = stream;
localMediaStream = stream;
}, onFailSoHard);
} else if (navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(\’video\’, function(stream) {
video.src = window.webkitURL.createObjectURL(stream);
localMediaStream = stream;
}, onFailSoHard);
} else {
onFailSoHard({target: video});
}
}, false);
document.querySelector(\’#stop-button2\’).addEventListener(\’click\’, function(e) {
video.pause();
localMediaStream.stop(); // Doesn\’t do anything in Chrome.
}, false);
video.addEventListener(\’click\’, changeFilter, false);
})();
Comments
请启用 JavaScript 以查看由 Disqus 强力驱动的评论。
var disqus_shortname = \’html5rocks\’;
var disqus_identifier = \’https://www.html5rocks.com/tutorials/getusermedia/intro/\’;
var disqus_url = \’https://www.html5rocks.com/tutorials/getusermedia/intro/\’;
var disqus_developer = 0;
var disqus_config = function () {
var funky_language_code_mapping = {
\’de\’: \’de_inf\’,
\’es\’: \’es_ES\’,
\’pt\’: \’pt_EU\’,
\’sr\’: \’sr_CYRL\’,
\’sv\’: \’sv_SE\’,
\’zh\’: \’zh_HANT\’
};
this.language = funky_language_code_mapping[\’zh\’] ||
\’zh\’;
this.callbacks.onReady = [ function () {
try {
ga(\’send\’, \’event\’, \’View comments\’);
} catch(err){}
} ];
this.callbacks.onNewComment = [ function (comment) {
try {
ga(\’send\’, \’event\’, \’Commented\’);
} catch(err){}
} ];
};
window.addEventListener(\’load\’, function(e) {
var c = document.createElement(\’script\’);
c.type = \’text/javascript\’;
c.src = \’https://\’ + disqus_shortname + \’.disqus.com/count.js\’;
c.async = true;
var s = document.getElementsByTagName(\’script\’)[0], sp = s.parentNode;
sp.insertBefore(c, s);
if (window.location.hash === \’#disqus_thread\’)
loadComments();
}, false);
var disqus_loaded = false;
function loadComments() {
if (disqus_loaded)
return;
disqus_loaded = true;
ga(\’send\’, \’event\’, \’Interactions\’, \’Comments\’, \’Comments Loaded\’);
var s = document.getElementsByTagName(\’script\’)[0], sp = s.parentNode;
var dsq = document.createElement(\’script\’);
dsq.type = \’text/javascript\’;
dsq.async = true;
var disqusContainer = document.getElementById(\’disqus\’);
disqusContainer.classList.add(\’active\’);
dsq.src = \’https://\’ + disqus_shortname + \’.disqus.com/embed.js\’;
sp.insertBefore(dsq, s);
}
function outgoing(url) {
try {
ga(\’send\’, \’event\’, \’Outbound Links\’ , url);
} catch(err){}
}
// Open external links (also that don\’t have a target defined) in a new tab.
var externLinks = document.querySelectorAll(\’article.tutorial a[href^=\”http\”]:not([target])\’);
for(var i = 0, a; a = externLinks[i]; ++i) {
a.target = \’_blank\’;
a.addEventListener(\’click\’, new Function(\’outgoing(\’ + \’\”\’ + a.href.replace(/.*?:\\/\\//g, \”\”) + \’\”\’ + \’);\’));
}
var loadCommentsButtons = document.querySelectorAll(\’.load-comments\’);
for(var l = 0; l < loadCommentsButtons.length; l++)
loadCommentsButtons[l].addEventListener(\'click\', loadComments);
window.isCompatible = function() {
return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
};
if(navigator.share !== undefined) {
document.addEventListener(\’DOMContentLoaded\’, e => {
var shareBox = document.querySelector(\’aside.share\’);
if(shareBox !== undefined) { shareBox.classList.toggle(\’native\’); }
var shareBtn = document.querySelector(\’aside.share a.android\’);
shareBtn.addEventListener(\’click\’, clickEvent => {
clickEvent.preventDefault();
const title = \”Capturing Audio & Video in HTML5\”;
const url = \”https://www.html5rocks.com/tutorials/getusermedia/intro/\”;
navigator.share({title: title, text: url, url: url})
.then(() => console.log(\’Successful share\’),
error => console.log(\’Error sharing:\’, error));
});
});
}
if (isCompatible() === false) {
document.getElementById(\’notcompatible\’).className = \’\’;
}
function _prettyPrint() {
if (typeof customPrettyPrintLanguage != \’undefined\’) {
customPrettyPrintLanguage();
}
prettyPrint();
}
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({\’gtm.start\’:
new Date().getTime(),event:\’gtm.js\’});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!=\’dataLayer\’?\’&l=\’+l:\’\’;j.async=true;j.src=
\’//www.googletagmanager.com/gtm.js?id=\’+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,\’script\’,\’dataLayer\’,\’GTM-MB3LRF\’);
(function() {
// Kill feedburner and marketing tracking arguments, but let them register
// before we do it.
setTimeout(function() {
if (/^\\?utm_/.test(document.location.search) &&
window.history.replaceState) {
window.history.replaceState(
{}, \’\’, document.location.href.replace(/\\?utm_.*/, \’\’));
}
}, 2000);
var siteHeader = document.getElementById(\’siteheader\’);
var navToggle = document.getElementById(\’navtoggle\’);
var siteNav = document.getElementById(\’sitenav\’);
function toggle(target, forceActive) {
if (typeof toc !== \’undefined\’) {
// Switch off whichever one is not the
// current target
if (target === toc)
siteNav.classList.remove(\’active\’);
else
toc.classList.remove(\’active\’);
}
// Toggle if no force parameter is set
if (typeof forceActive === \’undefined\’) {
target.classList.toggle(\’active\’);
} else {
if (forceActive)
target.classList.add(\’active\’);
else
target.classList.remove(\’active\’);
}
// now find out what the set state ended up being
var isActive = target.classList.contains(\’active\’);
if (isActive)
siteHeader.classList.add(\’expanded\’);
else
siteHeader.classList.remove(\’expanded\’);
}
navToggle.addEventListener(\’click\’, function(e) {
toggle(siteNav);
e.preventDefault();
});
var tocToggle = document.getElementById(\’toctoggle\’);
var toc = document.getElementById(\’toc\’);
var articleMeta = document.getElementById(\’article-meta\’);
var articleContent = document.getElementById(\’article-content\’);
var articleMetaHeight = 0;
var articleMetaMaxY = 0;
var articleMetaMinY = 0;
var articleContentPadding = 200;
var tocLinks = document.querySelectorAll(\’.toc a\’);
for (var t = 0; t = articleMetaMinY) {
articleMeta.classList.add(\’sticky\’);
var articleMetaTop = 22 – Math.max(0, window.scrollY – articleMetaMaxY);
articleMeta.style.top = articleMetaTop + \’px\’;
} else {
articleMeta.classList.remove(\’sticky\’);
articleMeta.style.top = \’auto\’;
}
}
if (articleMeta.getBoundingClientRect) {
setMinScrollYFromMetaY();
setMaxScrollYFromContentHeight();
document.addEventListener(\’scroll\’, onScroll);
window.addEventListener(\’load\’, setMaxScrollYFromContentHeight, false);
}
})();
(function(i,s,o,g,r,a,m){i[\’GoogleAnalyticsObject\’]=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,\’script\’,\’//www.google-analytics.com/analytics.js\’,\’ga\’);
ga(\’create\’, \’UA-15028909-1\’, \’auto\’);
ga(\’create\’, \’UA-49880327-4\’, \’auto\’, {\’name\’: \’html5rocks\’});
ga(\’send\’, \’pageview\’);
ga(\’html5rocks.send\’, \’pageview\’);
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({\’gtm.start\’:
new Date().getTime(),event:\’gtm.js\’});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!=\’dataLayer\’?\’&l=\’+l:\’\’;j.async=true;j.src=
\’//www.googletagmanager.com/gtm.js?id=\’+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,\’script\’,\’dataLayer\’,\’GTM-MB3LRF\’);
0