webkitMediaStream Object Type lost while using sendMessage in Chrome Extension

2.4k views Asked by At

So I'm trying to capture web audio from a tab and pass it into another script that works with DOM elements on the page.

EXTENSION SCRIPT

In the background.js, I use the following script:

    chrome.tabCapture.capture(constraints, function(stream) {
        console.log("\ngot stream");
        console.log(stream);

        chrome.tabs.sendMessage(tabID, {
            "message": "stream",
            "stream": stream
        });
    });

The Developer Toolkit shows me that the created object is indeed a MediaStream object. (Which I want and appears to be working fine).

EXTENSION CONSOLE:

MediaStream {onremovetrack: null, onaddtrack: null, onended: null, ended: false, id: "c0jm4lYJus3XCwQgesUGT9lpyPQiWlGKHb7q"…}

CONTENT SCRIPT

I use a content script (injected), on the page itself to then pull the JSON serialized object back out:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.message === "stream") {
    var thisStream = request.stream;
    console.log(thisStream);
    if (!thisStream) {
      console.log("stream is null");
      return;
    }    
    loadStream(thisStream);
  }
  else if (request.message === "statusChanged") {
    console.log("statusChanged");
  }
});

PAGE CONSOLE

Unfortunately, because of JSON serialization, the object type is lost:

Object {onremovetrack: null, onaddtrack: null, onended: null, ended: false, id: "c0jm4lYJus3XCwQgesUGT9lpyPQiWlGKHb7q"…}

I need the object recast as a MediaStream object and have tried the following things which all failed:

Attempt 1: FAILED

var stream = new webkitMediaStream;
function loadStream(thisStream) {
    stream = thisStream;
}

Attempt 2: FAILED

var stream;
function loadStream(thisStream) {
    stream = new webkitMediaStream(thisStream);
}

Attempt 3: FAILED

var stream;
function loadStream(thisStream) {
    stream = Object.create(webkitMediaStream, thisStream);
}

NOTE: The constructor for the MediaStream object IS webkitMediaStream.

I need either a better method for passing the object from the extension script (the only place the chrome.tab.capture() method works from) to the content script (the only place that has access to and can modify the DOM elements of the page),

OR

I need a way of recasting the JSON serialized object back into a fully functional MediaStream object.

Thanks in advance!

JRad the Bad

1

There are 1 answers

0
Rob W On

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 a blob:-URL for the stream and assign it to video.src to see a video. The URL created by URL.createObjectURL can only be used by a page at the same origin, so you need to create the <video> tag in a chrome-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 in web_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"
    ]
}