webkitMediaStream Object Type lost while using sendMessage in Chrome Extension
Extension messages are always JSON-serialized, so it's indeed obvious that you cannot send a MediaStream
from the background page to the web page. The question is, do you really need to send the MediaStream
from the background to the content script?
- If you only need to, e.g. display the video, then you can use
URL.createObjectURL
to get ablob:
-URL for the stream and assign it tovideo.src
to see a video. The URL created byURL.createObjectURL
can only be used by a page at the same origin, so you need to create the<video>
tag in achrome-extension://
page; either in a tab, or in a frame. If you want to do this in a frame, make sure that the page is listed inweb_accessible_resources
.
If you DO really need a MediaStream
object of the tab in the tab, then RTCPeerConnection
can be used to send the stream. This WebRTC API is normally used to exchange media streams between peers in a network, but it can also be used to send streams from one page to another page in another tab or browser.
Here's a full example. Visit any web page, and click on the extension button. Then the extension will insert a video in the page showing the current tab.
background.js
function sendStreamToTab(tabId, stream) { var pc = new webkitRTCPeerConnection({iceServers:[]}); pc.addStream(stream); pc.createOffer(function(offer) { pc.setLocalDescription(offer, function() { // Use chrome.tabs.connect instead of sendMessage // to make sure that the lifetime of the stream // is tied to the lifetime of the consumer (tab). var port = chrome.tabs.connect(tabId, {name: 'tabCaptureSDP'}); port.onDisconnect.addListener(function() { stopStream(stream); }); port.onMessage.addListener(function(sdp) { pc.setRemoteDescription(new RTCSessionDescription(sdp)); }); port.postMessage(pc.localDescription); }); });}function stopStream(stream) { var tracks = this.getTracks(); for (var i = 0; i < tracks.length; ++i) { tracks[i].stop(); }}function captureTab(tabId) { // Note: this method must be invoked by the user as defined // in https://crbug.com/489258, e.g. chrome.browserAction.onClicked. chrome.tabCapture.capture({ audio: true, video: true, audioConstraints: { mandatory: { chromeMediaSource: 'tab', }, }, videoConstraints: { mandatory: { chromeMediaSource: 'tab', }, }, }, function(stream) { if (!stream) { alert('Stream creation failed: ' + chrome.runtime.lastError.message); } chrome.tabs.executeScript(tabId, {file: 'contentscript.js'}, function() { if (chrome.runtime.lastError) { stopStream(stream); alert('Script injection failed:' + chrome.runtime.lastError.message); } else { sendStreamToTab(tabId, stream); } }); });}chrome.browserAction.onClicked.addListener(function(tab) { captureTab(tab.id);});
contentscript.js
function onReceiveStream(stream) { // Just to show that we can receive streams: var video = document.createElement('video'); video.style.border = '1px solid black'; video.src = URL.createObjectURL(stream); document.body.insertBefore(video, document.body.firstChild);}function onReceiveOfferSDP(sdp, sendResponse) { var pc = new webkitRTCPeerConnection({iceServers:[]}); pc.onaddstream = function(event) { onReceiveStream(event.stream); }; pc.setRemoteDescription(new RTCSessionDescription(sdp), function() { pc.createAnswer(function(answer) { pc.setLocalDescription(answer); sendResponse(pc.localDescription); }); });}// Run once to prevent the message from being handled twice when// executeScript is called multiple times.if (!window.hasRun) { window.hasRun = 1; chrome.runtime.onConnect.addListener(function(port) { if (port.name === 'tabCaptureSDP') { port.onMessage.addListener(function(remoteDescription) { onReceiveOfferSDP(remoteDescription, function(sdp) { port.postMessage(sdp); }); }); } });}
manifest.json
{ "name": "tabCapture to tab", "version": "1", "manifest_version": 2, "background": { "scripts": ["background.js"], "persistent": false }, "browser_action": { "default_title": "Capture tab" }, "permissions": [ "activeTab", "tabCapture" ]}