Added support for remote cast on the web media panel
This commit is contained in:
parent
8a75979f72
commit
d15b21ddfa
7 changed files with 271 additions and 27 deletions
|
@ -77,3 +77,51 @@ form#video-ctrl {
|
||||||
100% { background: #d4ffe3; }
|
100% { background: #d4ffe3; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[data-toggle="#media-devices-panel"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.remote[data-toggle="#media-devices-panel"] {
|
||||||
|
color: #34b868;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.selected[data-toggle="#media-devices-panel"] {
|
||||||
|
box-shadow: inset 0 2px 0 rgba(0,0,0,0.1),
|
||||||
|
inset 3px 0 0 rgba(0,0,0,0.1),
|
||||||
|
inset 0 0 2px rgba(0,0,0,0.1),
|
||||||
|
inset 0 0 0 1px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f0f0f0;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel .cast-device {
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel .cast-device-local {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel .cast-device.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #34b868;
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel .cast-device:hover {
|
||||||
|
background-color: #daf8e2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#media-devices-panel * > .cast-device-icon {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ $(document).ready(function() {
|
||||||
onWebsocketTimeout(websocket), websocketReconnectMsecs);
|
onWebsocketTimeout(websocket), websocketReconnectMsecs);
|
||||||
|
|
||||||
websocket.onmessage = function(event) {
|
websocket.onmessage = function(event) {
|
||||||
|
console.debug(event);
|
||||||
for (var listener of eventListeners) {
|
for (var listener of eventListeners) {
|
||||||
data = event.data;
|
data = event.data;
|
||||||
if (typeof event.data === 'string') {
|
if (typeof event.data === 'string') {
|
||||||
|
@ -218,6 +219,12 @@ function execute(request, onSuccess, onError, onComplete) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
success: function(response, status, xhr) {
|
success: function(response, status, xhr) {
|
||||||
|
if ('errors' in response && response.errors.length) {
|
||||||
|
if (onError) {
|
||||||
|
onError(xhr, '500', response.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(response, status, xhr);
|
onSuccess(response, status, xhr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var onEvent = function(event) {
|
var onEvent = function(event) {
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
switch (event.args.type) {
|
switch (event.args.type) {
|
||||||
case 'platypush.message.event.assistant.ConversationStartEvent':
|
case 'platypush.message.event.assistant.ConversationStartEvent':
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|
|
@ -4,6 +4,10 @@ $(document).ready(function() {
|
||||||
$videoResults = $('#video-results'),
|
$videoResults = $('#video-results'),
|
||||||
$volumeCtrl = $('#video-volume-ctrl'),
|
$volumeCtrl = $('#video-volume-ctrl'),
|
||||||
$ctrlForm = $('#video-ctrl'),
|
$ctrlForm = $('#video-ctrl'),
|
||||||
|
$devsPanel = $('#media-devices-panel'),
|
||||||
|
$devsBtn = $('button[data-toggle="#media-devices-panel"]'),
|
||||||
|
$searchBarContainer = $('#media-search-bar-container'),
|
||||||
|
$mediaBtnsContainer = $('#media-btns-container'),
|
||||||
prevVolume = undefined;
|
prevVolume = undefined;
|
||||||
|
|
||||||
var updateVideoResults = function(videos) {
|
var updateVideoResults = function(videos) {
|
||||||
|
@ -38,6 +42,19 @@ $(document).ready(function() {
|
||||||
return $iconContainer;
|
return $iconContainer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getSelectedDevice = function() {
|
||||||
|
var device = { isRemote: false, name: undefined };
|
||||||
|
var $remoteDevice = $devsPanel.find('.cast-device.selected')
|
||||||
|
.filter((i, dev) => !$(dev).data('local') && $(dev).data('name'));
|
||||||
|
|
||||||
|
if ($remoteDevice.length) {
|
||||||
|
device.isRemote = true;
|
||||||
|
device.name = $remoteDevice.data('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
return device;
|
||||||
|
};
|
||||||
|
|
||||||
var initBindings = function() {
|
var initBindings = function() {
|
||||||
$searchForm.on('submit', function(event) {
|
$searchForm.on('submit', function(event) {
|
||||||
var $input = $(this).find('input[name=video-search-text]');
|
var $input = $(this).find('input[name=video-search-text]');
|
||||||
|
@ -84,12 +101,18 @@ $(document).ready(function() {
|
||||||
var action = $(this).data('action');
|
var action = $(this).data('action');
|
||||||
var $btn = $(this);
|
var $btn = $(this);
|
||||||
|
|
||||||
execute(
|
var requestArgs = {
|
||||||
{
|
|
||||||
type: 'request',
|
type: 'request',
|
||||||
action: 'media.' + action,
|
action: 'media.' + action,
|
||||||
|
};
|
||||||
|
|
||||||
|
var selectedDevice = getSelectedDevice();
|
||||||
|
if (selectedDevice.isRemote) {
|
||||||
|
requestArgs.action = 'media.chromecast.' + action;
|
||||||
|
requestArgs.args = { 'chromecast': selectedDevice.name };
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
execute(requestArgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
$volumeCtrl.on('mousedown touchstart', function(event) {
|
$volumeCtrl.on('mousedown touchstart', function(event) {
|
||||||
|
@ -97,13 +120,19 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
$volumeCtrl.on('mouseup touchend', function(event) {
|
$volumeCtrl.on('mouseup touchend', function(event) {
|
||||||
execute(
|
var requestArgs = {
|
||||||
{
|
|
||||||
type: 'request',
|
type: 'request',
|
||||||
action: 'media.set_volume',
|
action: 'media.set_volume',
|
||||||
args: { volume: $(this).val() }
|
args: { volume: $(this).val() },
|
||||||
},
|
};
|
||||||
|
|
||||||
|
var selectedDevice = getSelectedDevice();
|
||||||
|
if (selectedDevice.isRemote) {
|
||||||
|
requestArgs.action = 'media.chromecast.set_volume',
|
||||||
|
requestArgs.args.chromecast = selectedDevice.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(requestArgs,
|
||||||
onSuccess=undefined,
|
onSuccess=undefined,
|
||||||
onError = function() {
|
onError = function() {
|
||||||
$volumeCtrl.val(prevVolume);
|
$volumeCtrl.val(prevVolume);
|
||||||
|
@ -120,14 +149,21 @@ $(document).ready(function() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$videoResults.text('Loading video...');
|
var requestArgs = {
|
||||||
execute(
|
|
||||||
{
|
|
||||||
type: 'request',
|
type: 'request',
|
||||||
action: 'media.play',
|
action: 'media.play',
|
||||||
args: { resource: $item.data('url') },
|
args: { resource: $item.data('url') },
|
||||||
},
|
};
|
||||||
|
|
||||||
|
var selectedDevice = getSelectedDevice();
|
||||||
|
if (selectedDevice.isRemote) {
|
||||||
|
requestArgs.action = 'media.chromecast.play';
|
||||||
|
requestArgs.args.chromecast = selectedDevice.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$videoResults.text('Loading video...');
|
||||||
|
execute(
|
||||||
|
requestArgs,
|
||||||
function() {
|
function() {
|
||||||
$videoResults.html(results);
|
$videoResults.html(results);
|
||||||
$item.siblings().removeClass('active');
|
$item.siblings().removeClass('active');
|
||||||
|
@ -139,9 +175,82 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$devsBtn.on('click touch', function() {
|
||||||
|
$(this).toggleClass('selected');
|
||||||
|
$devsPanel.css('top', ($(this).position().top + $(this).outerHeight()) + 'px');
|
||||||
|
$devsPanel.css('left', ($(this).position().left) + 'px');
|
||||||
|
$devsPanel.toggle();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$devsPanel.on('mouseup touchend', '.cast-device', function() {
|
||||||
|
var $devices = $devsPanel.find('.cast-device');
|
||||||
|
var $curSelected = $devices.filter((i, d) => $(d).hasClass('selected'));
|
||||||
|
|
||||||
|
if ($curSelected.data('name') !== $(this).data('name')) {
|
||||||
|
$curSelected.removeClass('selected');
|
||||||
|
$(this).addClass('selected');
|
||||||
|
|
||||||
|
if (!$(this).data('local')) {
|
||||||
|
$devsBtn.addClass('remote');
|
||||||
|
} else {
|
||||||
|
$devsBtn.removeClass('remote');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$devsPanel.hide();
|
||||||
|
$devsBtn.removeClass('selected');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initRemoteDevices = function() {
|
||||||
|
execute(
|
||||||
|
{
|
||||||
|
type: 'request',
|
||||||
|
action: 'media.chromecast.get_chromecasts',
|
||||||
|
},
|
||||||
|
|
||||||
|
function(results) {
|
||||||
|
if (!results || results.response.errors.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$searchBarContainer.removeClass('eleven').addClass('nine');
|
||||||
|
$mediaBtnsContainer.removeClass('one').addClass('three');
|
||||||
|
$devsBtn.show();
|
||||||
|
|
||||||
|
results = results.response.output;
|
||||||
|
for (var cast of results) {
|
||||||
|
var $cast = $('<div></div>').addClass('row cast-device')
|
||||||
|
.addClass('cast-device-' + cast.type).data('name', cast.name);
|
||||||
|
|
||||||
|
var icon = 'question';
|
||||||
|
switch (cast.type) {
|
||||||
|
case 'cast': icon = 'tv'; break;
|
||||||
|
case 'audio': icon = 'volume-up'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $castIcon = $('<i></i>').addClass('fa fa-' + icon)
|
||||||
|
.addClass('cast-device-icon');
|
||||||
|
var $castName = $('<span></span>').addClass('cast-device-name')
|
||||||
|
.text(cast.name);
|
||||||
|
|
||||||
|
var $iconContainer = $('<div></div>').addClass('two columns');
|
||||||
|
var $nameContainer = $('<div></div>').addClass('ten columns');
|
||||||
|
$castIcon.appendTo($iconContainer);
|
||||||
|
$castName.appendTo($nameContainer);
|
||||||
|
|
||||||
|
$iconContainer.appendTo($cast);
|
||||||
|
$nameContainer.appendTo($cast);
|
||||||
|
$cast.appendTo($devsPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
var init = function() {
|
var init = function() {
|
||||||
|
initRemoteDevices();
|
||||||
initBindings();
|
initBindings();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,27 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="eleven columns">
|
<div class="eleven columns" id="media-search-bar-container">
|
||||||
<input type="text" name="video-search-text" placeholder="Search query or video URL">
|
<input type="text" name="video-search-text" placeholder="Search query or video URL">
|
||||||
</div>
|
</div>
|
||||||
<div class="one column">
|
<div class="one columns" id="media-btns-container">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button data-toggle="#media-devices-panel">
|
||||||
|
<i class="fa fa-tv"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" id="media-devices-panel">
|
||||||
|
<div class="row cast-device cast-device-local selected" data-local="local">
|
||||||
|
<div class="two columns">
|
||||||
|
<i class="fa fa-microchip cast-device-icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ten columns">
|
||||||
|
<span class="cast-device-name">This device</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -36,14 +50,10 @@
|
||||||
<i class="fa fa-step-backward"></i>
|
<i class="fa fa-step-backward"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button data-action="backward">
|
<button data-action="back">
|
||||||
<i class="fa fa-backward"></i>
|
<i class="fa fa-backward"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button data-action="play">
|
|
||||||
<i class="fa fa-play"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button data-action="pause">
|
<button data-action="pause">
|
||||||
<i class="fa fa-pause"></i>
|
<i class="fa fa-pause"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
71
platypush/plugins/media/bin/localstream
Executable file
71
platypush/plugins/media/bin/localstream
Executable file
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Requires:
|
||||||
|
// - express (`npm install express`)
|
||||||
|
// - mime-types (`npm install mime-types`)
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const process = require('process')
|
||||||
|
const mime = require('mime-types')
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
function parseArgv() {
|
||||||
|
let file = undefined
|
||||||
|
let port = 8989
|
||||||
|
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
throw Error(`Usage: ${process.argv[0]} ${process.argv[1]} <media_file> [port=${port}]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
file = process.argv[2]
|
||||||
|
|
||||||
|
if (process.argv.length > 3) {
|
||||||
|
port = parseInt(process.argv[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
return { file: file, port: port }
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = parseArgv()
|
||||||
|
|
||||||
|
app.get('/video', function(req, res) {
|
||||||
|
const path = args.file
|
||||||
|
const ext = args.file.split('.').pop()
|
||||||
|
const stat = fs.statSync(path)
|
||||||
|
const fileSize = stat.size
|
||||||
|
const range = req.headers.range
|
||||||
|
const mimeType = mime.lookup(ext)
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
const parts = range.replace(/bytes=/, "").split("-")
|
||||||
|
const start = parseInt(parts[0], 10)
|
||||||
|
const end = parts[1]
|
||||||
|
? parseInt(parts[1], 10)
|
||||||
|
: fileSize-1
|
||||||
|
|
||||||
|
const chunksize = (end-start)+1
|
||||||
|
const file = fs.createReadStream(path, {start, end})
|
||||||
|
const head = {
|
||||||
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Length': chunksize,
|
||||||
|
'Content-Type': mimeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(206, head)
|
||||||
|
file.pipe(res)
|
||||||
|
} else {
|
||||||
|
const head = {
|
||||||
|
'Content-Length': fileSize,
|
||||||
|
'Content-Type': mimeType,
|
||||||
|
}
|
||||||
|
res.writeHead(200, head)
|
||||||
|
fs.createReadStream(path).pipe(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(args.port, function () {
|
||||||
|
console.log(`Listening on port ${args.port}`)
|
||||||
|
})
|
|
@ -67,6 +67,7 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
'name': cc.app_display_name,
|
'name': cc.app_display_name,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'media': self.status(cc.name).output,
|
||||||
'is_active_input': cc.status.is_active_input,
|
'is_active_input': cc.status.is_active_input,
|
||||||
'is_stand_by': cc.status.is_stand_by,
|
'is_stand_by': cc.status.is_stand_by,
|
||||||
'is_idle': cc.is_idle,
|
'is_idle': cc.is_idle,
|
||||||
|
|
Loading…
Reference in a new issue