Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 10b1fdf

Browse files
committed
Bug 1129785 - support video sharing via Presentation API. r=mfinkle.
--HG-- extra : commitid : tt4LYaWaUe
1 parent a611774 commit 10b1fdf

6 files changed

Lines changed: 284 additions & 1 deletion

File tree

mobile/android/app/mobile.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ pref("network.predictor.enabled", true);
116116
pref("network.predictor.max-db-size", 2097152); // bytes
117117
pref("network.predictor.preserve", 50); // percentage of predictor data to keep when cleaning up
118118

119+
// Use JS mDNS as a fallback
120+
pref("network.mdns.use_js_fallback", true);
121+
119122
/* history max results display */
120123
pref("browser.display.history.maxresults", 100);
121124

@@ -976,3 +979,7 @@ pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com
976979

977980
// Token server used by Firefox Account-authenticated Sync.
978981
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
982+
983+
// Enable Presentation API
984+
pref("dom.presentation.enabled", true);
985+
pref("dom.presentation.discovery.enabled", true);

mobile/android/chrome/content/CastingApps.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,64 @@ var mediaPlayerDevice = {
5959
}
6060
};
6161

62+
var fxOSTVDevice = {
63+
id: "app://fling-player.gaiamobile.org",
64+
target: "app://fling-player.gaiamobile.org/index.html",
65+
factory: function(aService) {
66+
Cu.import("resource://gre/modules/PresentationApp.jsm");
67+
let request = new window.PresentationRequest(this.target);
68+
return new PresentationApp(aService, request);
69+
},
70+
init: function() {
71+
Services.obs.addObserver(this, "presentation-device-change", false);
72+
SimpleServiceDiscovery.addExternalDiscovery(this);
73+
},
74+
observe: function(subject, topic, data) {
75+
let device = subject.QueryInterface(Ci.nsIPresentationDevice);
76+
let service = this.toService(device);
77+
switch (data) {
78+
case "add":
79+
SimpleServiceDiscovery.addService(service);
80+
break;
81+
case "update":
82+
SimpleServiceDiscovery.updateService(service);
83+
break;
84+
case "remove":
85+
if(SimpleServiceDiscovery.findServiceForID(device.id)) {
86+
SimpleServiceDiscovery.removeService(device.id);
87+
}
88+
break;
89+
}
90+
},
91+
toService: function(device) {
92+
return {
93+
location: device.id,
94+
target: fxOSTVDevice.target,
95+
friendlyName: device.name,
96+
uuid: device.id,
97+
manufacturer: "Firefox OS TV",
98+
modelName: "Firefox OS TV",
99+
};
100+
},
101+
startDiscovery: function() {
102+
window.navigator.mozPresentationDeviceInfo.forceDiscovery();
103+
104+
// need to update the lastPing time for known device.
105+
window.navigator.mozPresentationDeviceInfo.getAll()
106+
.then(function(devices) {
107+
for (let device of devices) {
108+
let service = fxOSTVDevice.toService(device);
109+
SimpleServiceDiscovery.addService(service);
110+
}
111+
});
112+
},
113+
stopDiscovery: function() {
114+
// do nothing
115+
},
116+
types: ["video/mp4", "video/webm"],
117+
extensions: ["mp4", "webm"],
118+
};
119+
62120
var CastingApps = {
63121
_castMenuId: -1,
64122
mirrorStartMenuId: -1,
@@ -79,6 +137,10 @@ var CastingApps = {
79137
mediaPlayerDevice.init();
80138
SimpleServiceDiscovery.registerDevice(mediaPlayerDevice);
81139

140+
// Presentation Device will notify us any time the available device list changes.
141+
fxOSTVDevice.init();
142+
SimpleServiceDiscovery.registerDevice(fxOSTVDevice);
143+
82144
// Search for devices continuously
83145
SimpleServiceDiscovery.search(this._interval);
84146

mobile/android/chrome/content/browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ var BrowserApp = {
591591

592592
InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
593593
InitLater(() => Cu.import("resource://gre/modules/Payment.jsm"));
594+
InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
594595

595596
InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
596597
InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));

toolkit/modules/moz.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ EXTRA_JS_MODULES += [
6363
'RemoteSecurityUI.jsm',
6464
'RemoteWebProgress.jsm',
6565
'ResetProfile.jsm',
66+
'secondscreen/PresentationApp.jsm',
6667
'secondscreen/RokuApp.jsm',
6768
'secondscreen/SimpleServiceDiscovery.jsm',
6869
'SelectContentHelper.jsm',
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2+
/* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at http://mozilla.org/MPL/2.0/. */
5+
6+
"use strict";
7+
8+
this.EXPORTED_SYMBOLS = ["PresentationApp"];
9+
10+
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
11+
12+
Cu.import("resource://gre/modules/Services.jsm");
13+
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14+
15+
XPCOMUtils.defineLazyGetter(this, "sysInfo", () => {
16+
return Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
17+
});
18+
19+
const DEBUG = false;
20+
21+
const STATE_UNINIT = "uninitialized" // RemoteMedia status
22+
const STATE_STARTED = "started"; // RemoteMedia status
23+
const STATE_PAUSED = "paused"; // RemoteMedia status
24+
const STATE_SHUTDOWN = "shutdown"; // RemoteMedia status
25+
26+
function debug(msg) {
27+
Services.console.logStringMessage("PresentationApp: " + msg);
28+
}
29+
30+
// PresentationApp is a wrapper for interacting with a Presentation Receiver Device.
31+
function PresentationApp(service, request) {
32+
this.service = service;
33+
this.request = request;
34+
}
35+
36+
PresentationApp.prototype = {
37+
start: function start(callback) {
38+
this.request.startWithDevice(this.service.uuid)
39+
.then((session) => {
40+
this._session = session;
41+
if (callback) {
42+
callback(true);
43+
}
44+
}, () => {
45+
if (callback) {
46+
callback(false);
47+
}
48+
});
49+
},
50+
51+
stop: function stop(callback) {
52+
if (this._session && this._session.state === "connected") {
53+
this._session.terminate();
54+
}
55+
56+
delete this._session;
57+
58+
if (callback) {
59+
callback(true);
60+
}
61+
},
62+
63+
remoteMedia: function remoteMedia(callback, listener) {
64+
if (callback) {
65+
if (!this._session) {
66+
callback();
67+
return;
68+
}
69+
70+
callback(new RemoteMedia(this._session, listener));
71+
}
72+
}
73+
}
74+
75+
/* RemoteMedia provides a wrapper for using Presentation API to control Firefox TV app.
76+
* The server implementation must be built into the Firefox TV receiver app.
77+
* see https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/fling-player
78+
*/
79+
function RemoteMedia(session, listener) {
80+
this._session = session ;
81+
this._listener = listener;
82+
this._status = STATE_UNINIT;
83+
84+
this._session.addEventListener("message", this);
85+
this._session.addEventListener("statechange", this);
86+
87+
if (this._listener && "onRemoteMediaStart" in this._listener) {
88+
Services.tm.mainThread.dispatch((function() {
89+
this._listener.onRemoteMediaStart(this);
90+
}).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
91+
}
92+
}
93+
94+
RemoteMedia.prototype = {
95+
_seq: 0,
96+
97+
handleEvent: function(e) {
98+
switch (e.type) {
99+
case "message":
100+
this._onmessage(e);
101+
break;
102+
case "statechange":
103+
this._onstatechange(e);
104+
break;
105+
}
106+
},
107+
108+
_onmessage: function(e) {
109+
DEBUG && debug("onmessage: " + e.data);
110+
if (this.status === STATE_SHUTDOWN) {
111+
return;
112+
}
113+
114+
if (e.data.indexOf("stopped") > -1) {
115+
if (this.status !== STATE_PAUSED) {
116+
this._status = STATE_PAUSED;
117+
if (this._listener && "onRemoteMediaStatus" in this._listener) {
118+
this._listener.onRemoteMediaStatus(this);
119+
}
120+
}
121+
} else if (e.data.indexOf("playing") > -1) {
122+
if (this.status !== STATE_STARTED) {
123+
this._status = STATE_STARTED;
124+
if (this._listener && "onRemoteMediaStatus" in this._listener) {
125+
this._listener.onRemoteMediaStatus(this);
126+
}
127+
}
128+
}
129+
},
130+
131+
_onstatechange: function(e) {
132+
DEBUG && debug("onstatechange: " + this._session.state);
133+
if (this._session.state !== "connected") {
134+
this._status = STATE_SHUTDOWN;
135+
if (this._listener && "onRemoteMediaStop" in this._listener) {
136+
this._listener.onRemoteMediaStop(this);
137+
}
138+
}
139+
},
140+
141+
_sendCommand: function(command, data) {
142+
let msg = {
143+
'type': command,
144+
'seq': ++this._seq
145+
};
146+
147+
if (data) {
148+
for (var k in data) {
149+
msg[k] = data[k];
150+
}
151+
}
152+
153+
let raw = JSON.stringify(msg);
154+
DEBUG && debug("send command: " + raw);
155+
156+
this._session.send(raw);
157+
},
158+
159+
shutdown: function shutdown() {
160+
DEBUG && debug("RemoteMedia - shutdown");
161+
this._sendCommand("close");
162+
},
163+
164+
play: function play() {
165+
DEBUG && debug("RemoteMedia - play");
166+
this._sendCommand("play");
167+
},
168+
169+
pause: function pause() {
170+
DEBUG && debug("RemoteMedia - pause");
171+
this._sendCommand("pause");
172+
},
173+
174+
load: function load(data) {
175+
DEBUG && debug("RemoteMedia - load: " + data);
176+
this._sendCommand("load", { "url": data.source });
177+
178+
let deviceName;
179+
if (Services.appinfo.widgetToolkit == "android") {
180+
deviceName = sysInfo.get("device");
181+
} else {
182+
deviceName = sysInfo.get("host");
183+
}
184+
this._sendCommand("device-info", { "displayName": deviceName });
185+
},
186+
187+
get status() {
188+
return this._status;
189+
}
190+
}

toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ var SimpleServiceDiscovery = {
5757
_searchTimestamp: 0,
5858
_searchTimeout: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
5959
_searchRepeat: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
60+
_discoveryMethods: [],
6061

6162
_forceTrailingSlash: function(aURL) {
6263
// Cleanup the URL to make it consistent across devices
@@ -141,6 +142,9 @@ var SimpleServiceDiscovery = {
141142
// UDP broadcasts, so this is a way to skip discovery.
142143
this._searchFixedDevices();
143144

145+
// Look for any devices via registered external discovery mechanism.
146+
this._startExternalDiscovery();
147+
144148
// Perform a UDP broadcast to search for SSDP devices
145149
let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(Ci.nsIUDPSocket);
146150
try {
@@ -226,6 +230,8 @@ var SimpleServiceDiscovery = {
226230
}
227231
}
228232
}
233+
234+
this._stopExternalDiscovery();
229235
},
230236

231237
getSupportedExtensions: function() {
@@ -409,5 +415,21 @@ var SimpleServiceDiscovery = {
409415

410416
// Make sure we remember this service is not stale
411417
this._services.get(service.uuid).lastPing = this._searchTimestamp;
412-
}
418+
},
419+
420+
addExternalDiscovery: function(discovery) {
421+
this._discoveryMethods.push(discovery);
422+
},
423+
424+
_startExternalDiscovery: function() {
425+
for (let discovery of this._discoveryMethods) {
426+
discovery.startDiscovery();
427+
}
428+
},
429+
430+
_stopExternalDiscovery: function() {
431+
for (let discovery of this._discoveryMethods) {
432+
discovery.stopDiscovery();
433+
}
434+
},
413435
}

0 commit comments

Comments
 (0)