platypush/platypush/backend/http/static/js/media.js

646 lines
22 KiB
JavaScript

$(document).ready(function() {
var $container = $('#video-container'),
$searchForm = $('#video-search'),
$videoResults = $('#video-results'),
$volumeCtrl = $('#video-volume-ctrl'),
$ctrlForm = $('#video-ctrl'),
$devsPanel = $('#media-devices-panel'),
$devsList = $devsPanel.find('.devices-list'),
$devsBtn = $('button[data-panel="#media-devices-panel"]'),
$devsBtnIcon = $('#media-devices-panel-icon'),
$devsRefreshBtn = $devsPanel.find('.refresh-devices'),
$searchBarContainer = $('#media-search-bar-container'),
$mediaBtnsContainer = $('#media-btns-container'),
$mediaItemPanel = $('#media-item-panel'),
$mediaSubtitlesModal = $('#media-subtitles-modal'),
$mediaSubtitlesResultsContainer = $mediaSubtitlesModal.find('.media-subtitles-results-container'),
$mediaSubtitlesResults = $mediaSubtitlesModal.find('.media-subtitles-results'),
$mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'),
$mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'),
prevVolume = undefined,
selectedResource = undefined;
const onEvent = (event) => {
switch (event.args.type) {
case 'platypush.message.event.media.MediaPlayRequestEvent':
createNotification({
'icon': 'stream',
'html': 'Processing media' + ('resource' in event.args
? ' ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPlayEvent':
createNotification({
'icon': 'play',
'html': 'Starting media playback' + ('resource' in event.args
? ' for ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPauseEvent':
createNotification({
'icon': 'pause',
'html': 'Media playback paused',
});
break;
case 'platypush.message.event.media.MediaStopEvent':
createNotification({
'icon': 'stop',
'html': 'Media playback stopped',
});
break;
}
};
const updateVideoResults = function(videos) {
$videoResults.html('');
for (var video of videos) {
var $videoResult = $('<div></div>')
.addClass('video-result')
.attr('data-url', video['url'])
.attr('data-panel', '#media-item-panel')
.html('title' in video ? video['title'] : video['url']);
var $icon = getVideoIconByUrl(video['url']);
$icon.prependTo($videoResult);
$videoResult.appendTo($videoResults);
}
};
const getVideoIconByUrl = function(url) {
var $icon = $('<i class="fa"></i>');
if (url.startsWith('file://')) {
$icon.addClass('fa-download');
} else if (url.startsWith('https://www.youtube.com/')) {
$icon.addClass('fa-youtube');
} else if (url.startsWith('magnet:?')) {
$icon.addClass('fa-film');
} else {
$icon.addClass('fa-video');
}
var $iconContainer = $('<span></span>').addClass('video-icon-container');
$icon.appendTo($iconContainer);
return $iconContainer;
};
const getSelectedDevice = function() {
var device = { isBrowser: false, isRemote: false, name: undefined };
var $remoteDevice = $devsPanel.find('.cast-device.selected')
.filter((i, dev) => !$(dev).data('local') && !$(dev).data('browser') && $(dev).data('name'));
var $browserDevice = $devsPanel.find('.cast-device.selected')
.filter((i, dev) => $(dev).data('browser'));
if ($remoteDevice.length) {
device.isRemote = true;
device.name = $remoteDevice.data('name');
} else if ($browserDevice.length) {
device.isBrowser = true;
}
return device;
};
const startStreamingTorrent = function(torrent) {
return new Promise((resolve, reject) => {
execute(
{
type: 'request',
action: 'media.webtorrent.play',
args: {
resource: torrent,
download_only: true,
}
},
(response) => {
resolve(response.response.output.url);
},
(error) => {
reject(error);
}
);
});
};
const startStreaming = function(media) {
if (media.startsWith('magnet:?')) {
return new Promise((resolve, reject) => {
startStreamingTorrent(media)
.then((url) => { resolve(url); })
.catch((error) => { reject(error); });
});
}
return new Promise((resolve, reject) => {
$.ajax({
type: 'PUT',
url: '/media',
contentType: 'application/json',
data: JSON.stringify({ source: media }),
complete: (xhr, textStatus) => {
if (xhr.status == 200) {
var uri = xhr.responseJSON.url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1]
resolve(uri);
} else {
reject(Error(xhr.responseText));
}
},
});
});
};
const stopStreaming = function(media_id) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'DELETE',
url: '/media/' + media_id,
contentType: 'application/json',
});
});
};
const getSubtitles = function(resource) {
return new Promise((resolve, reject) => {
if (!window.config.media.subtitles) {
return; // media.subtitles plugin not configured
}
run({
action: 'media.subtitles.get_subtitles',
args: { 'resource': resource }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const downloadSubtitles = function(link, mediaResource) {
return new Promise((resolve, reject) => {
run({
action: 'media.subtitles.download',
args: {
'link': link,
'media_resource': mediaResource,
}
}).then((response) => {
resolve(response.response.output.filename);
}).catch((error) => {
reject(error.message);
});
});
};
const setSubtitles = (filename) => {
return new Promise((resolve, reject) => {
run({
action: 'media.set_subtitles',
args: { 'filename': filename }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const playOnChromecast = (resource, device, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.chromecast.play',
args: {
'resource': resource,
'chromecast': device,
},
};
// TODO support for subtitles on Chromecast through internal streaming server
run(requestArgs).then((response) => {
resolve(response);
}).catch((error) => {
reject(error.message);
});
});
};
const playInBrowser = (resource, subtitles) => {
return new Promise((resolve, reject) => {
// TODO support for subtitles in local player
startStreaming(resource).then((url) => {
window.open(url, '_blank');
resolve(url);
}).catch((xhr, status, error) => {
reject(xhr.responseText);
});
});
};
const playOnServer = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.play',
args: { 'resource': resource },
};
if (subtitles) {
requestArgs.args.subtitles = subtitles;
}
run(requestArgs).then((response) => {
resolve(resource);
}).catch((error) => {
reject(error.message);
});
});
};
const _play = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var playHndl;
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
playHndl = playInBrowser(resource, subtitles);
} else if (selectedDevice.isRemote) {
playHndl = playOnChromecast(resource, selectedDevice.name, subtitles);
} else {
playHndl = playOnServer(resource, subtitles);
}
playHndl.then((response) => {
resolve(resource);
}).catch((error) => {
showError('Playback error: ' + error.message);
reject(error.message);
});
});
};
const play = (resource) => {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = () => { $videoResults.text('Loading video...'); };
var onVideoReady = () => {
$videoResults.html(results);
resolve(resource);
};
$mediaSearchSubtitles.data('resource', resource);
onVideoLoading();
var subtitlesConf = window.config.media.subtitles;
// TODO populate the subtitles panel and show the subtitles button
if (subtitlesConf && 'language' in subtitlesConf) {
tryFetchSubtitles(resource).then((subtitles) => {
if (!subtitles) {
showError('Cannot get subtitles: ' + error);
_play(resource).finally(onVideoReady);
} else {
_play(resource, subtitles).finally(onVideoReady);
}
});
} else {
_play(resource).finally(onVideoReady);
}
});
};
const tryFetchSubtitles = (resource) => {
return new Promise((resolve, reject) => {
getSubtitles(resource).then((subs) => {
if (!subs) {
resolve();
return; // No subtitles found
}
downloadSubtitles(subs[0].SubDownloadLink, resource).then((filename) => {
resolve(filename);
}).catch((error) => {
resolve();
});
});
});
};
const download = function(resource) {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = function() {
$videoResults.text('Loading video...');
};
var onVideoReady = function() {
$videoResults.html(results);
};
onVideoLoading();
startStreaming(resource)
.then((url) => {
url = url + '?download=1'
window.open(url, '_blank');
resolve(url);
})
.catch((xhr, status, error) => {
reject(xhr.responseText);
})
.finally(() => {
onVideoReady();
});
});
};
const initBindings = function() {
window.registerEventListener(onEvent);
$searchForm.on('submit', function(event) {
var $input = $(this).find('input[name=video-search-text]');
var resource = $input.val();
var request = {}
var onSuccess = function() {};
var onError = function() {};
var onComplete = function() {
$input.prop('disabled', false);
};
$input.prop('disabled', true);
$videoResults.text('Searching...');
if (resource.match(new RegExp('^https?://')) ||
resource.match(new RegExp('^file://'))) {
var videos = [{ url: resource }];
updateVideoResults(videos);
request = {
type: 'request',
action: 'media.play',
args: { resource: resource }
};
} else {
request = {
type: 'request',
action: 'media.search',
args: { query: resource }
};
onSuccess = function(response) {
var videos = response.response.output;
updateVideoResults(videos);
};
}
execute(request, onSuccess, onError, onComplete)
return false;
});
$ctrlForm.on('submit', function() { return false; });
$ctrlForm.find('button[data-action]').on('click touch', function(evt) {
var action = $(this).data('action');
var $btn = $(this);
var requestArgs = {
type: 'request',
action: 'media.' + action,
};
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
return; // The in-browser player can be used to control media
}
if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.' + action;
requestArgs.args = { 'chromecast': selectedDevice.name };
}
execute(requestArgs);
});
$volumeCtrl.on('mousedown touchstart', function(event) {
prevVolume = $(this).val();
});
$volumeCtrl.on('mouseup touchend', function(event) {
var requestArgs = {
type: 'request',
action: 'media.set_volume',
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,
onError = function() {
$volumeCtrl.val(prevVolume);
}
);
});
$videoResults.on('mousedown touchstart', '.video-result', function() {
selectedResource = $(this).data('url');
});
$videoResults.on('mouseup touchend', '.video-result', function(event) {
var $item = $(this);
var resource = $item.data('url');
if (resource !== selectedResource) {
return; // Did not really click this item
}
$item.siblings().removeClass('selected');
$item.addClass('selected');
$mediaItemPanel.css('top', (event.clientY + $(window).scrollTop()) + 'px');
$mediaItemPanel.css('left', (event.clientX + $(window).scrollLeft()) + 'px');
$mediaItemPanel.data('resource', resource);
});
$devsBtn.on('click touch', function() {
$(this).toggleClass('selected');
$devsPanel.css('top', ($(this).position().top + $(this).outerHeight()) + 'px');
$devsPanel.css('left', ($(this).position().left) + 'px');
return false;
});
$devsPanel.on('mouseup touchend', '.cast-device', function() {
if ($(this).hasClass('disabled')) {
return;
}
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');
$devsBtnIcon.attr('class', $(this).find('.fa').attr('class'));
if ($(this).data('browser') || $(this).data('local')) {
$devsBtn.removeClass('remote');
} else {
$devsBtn.addClass('remote');
}
// TODO Logic for switching destination on the fly
}
$devsPanel.hide();
$devsBtn.removeClass('selected');
});
$devsRefreshBtn.on('click', function() {
if ($(this).hasClass('disabled')) {
return;
}
$(this).addClass('disabled');
initRemoteDevices();
});
$mediaItemPanel.on('click', '[data-action]', function() {
var $action = $(this);
if ($action.hasClass('disabled')) {
return;
}
var action = $action.data('action');
var resource = $mediaItemPanel.data('resource');
if (!resource) {
return;
}
$mediaItemPanel.hide();
$mediaItemPanel.find('[data-action]').addClass('disabled');
eval(action)($mediaItemPanel.data('resource'))
.finally(() => {
$mediaItemPanel.find('[data-action]').removeClass('disabled');
});
});
$mediaSearchSubtitles.on('mouseup touchend', () => {
var resource = $mediaSearchSubtitles.data('resource');
if (!resource) {
return;
}
$mediaSubtitlesMessage.text('Loading subtitles...');
$mediaSubtitlesResults.text('');
$mediaSubtitlesMessage.show();
$mediaSubtitlesResultsContainer.hide();
getSubtitles(resource).then((subs) => {
if (!subs.length) {
$mediaSubtitlesMessage.text('No subtitles found');
return;
}
for (var sub of subs) {
var flagCode;
if ('ISO639' in sub) {
switch(sub.ISO639) {
case 'en': flagCode = 'gb'; break;
default: flagCode = sub.ISO639; break;
}
}
var $subContainer = $('<div></div>').addClass('row media-subtitle-container')
.data('download-link', sub.SubDownloadLink)
.data('resource', resource);
var $subFlagIconContainer = $('<div></div>').addClass('one column');
var $subFlagIcon = $('<span></span>')
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
sub.IsLocal ? 'fa fa-download' : ''))
.text(!(flagCode || sub.IsLocal) ? '?' : '');
var $subMovieName = $('<div></div>').addClass('five columns')
.text(sub.MovieName);
var $subFileName = $('<div></div>').addClass('six columns')
.text(sub.SubFileName);
$subFlagIcon.appendTo($subFlagIconContainer);
$subFlagIconContainer.appendTo($subContainer);
$subMovieName.appendTo($subContainer);
$subFileName.appendTo($subContainer);
$subContainer.appendTo($mediaSubtitlesResults);
}
$mediaSubtitlesMessage.hide();
$mediaSubtitlesResultsContainer.show();
}).catch((error) => {
$mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message);
});
});
};
const initRemoteDevices = function() {
$devsList.find('.cast-device[data-remote]').addClass('disabled');
execute(
{
type: 'request',
action: 'media.chromecast.get_chromecasts',
},
function(results) {
$devsRefreshBtn.removeClass('disabled');
$devsList.find('.cast-device[data-remote]').remove();
if (!results || results.response.errors.length) {
return;
}
results = results.response.output;
for (var cast of results) {
var $cast = $('<div></div>').addClass('row cast-device')
.addClass('cast-device-' + cast.type).attr('data-remote', true)
.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($devsList);
}
}
);
};
const init = function() {
initRemoteDevices();
initBindings();
};
init();
});