Copyright (C) 2006-2021 Ricardo Villalba <ricardo@smplayer.info>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "chromecast.h"
#include "links.h"
#include <QUrl>
#include <QFileInfo>
#include <QDesktopServices>
#include <QNetworkInterface>
#include <QProcess>
#include <QDir>
#include <QSettings>
#include <QUdpSocket>
#include <QHostAddress>
#include <QDebug>
#include "helper.h"
#ifdef CC_USE_TRAY_ICON
#include <QSystemTrayIcon>
#include <QMenu>
#include "images.h"
#endif
#ifdef USE_QRCODE
#include "qrcode/openwithdevicedialog.h"
#endif
#ifdef CONVERT_TO_VTT
#include "subreader.h"
#endif
#if !defined(CHROMECAST_USE_SERVER_WEBFSD) && !defined(CHROMECAST_USE_SERVER_SIMPLE_WEB_SERVER)
#define CHROMECAST_USE_SERVER_SIMPLE_WEB_SERVER
#endif
Chromecast * Chromecast::instance_obj = 0;
Chromecast * Chromecast::instance() {
if (instance_obj == 0) {
instance_obj = new Chromecast();
}
return instance_obj;
}
void Chromecast::deleteInstance() {
if (instance_obj) {
delete instance_obj;
instance_obj = 0;
}
}
Chromecast::Chromecast(QObject * parent)
: QObject(parent)
, server_process(0)
, settings(0)
, server_port(8010)
, directory_listing(true)
, server_needs_restart(false)
, output_device(DChromecast)
{
#ifdef CONVERT_TO_VTT
autoconvert_to_vtt = true;
sub_encoding = "ISO-8859-1";
sub_position = 95;
overwrite_vtt = true;
use_sub_filter = true;
#endif
#ifdef CC_USE_TRAY_ICON
tray_icon = new QSystemTrayIcon(parent);
tray_icon->setIcon(Images::icon("cast", 22));
tray_icon->setToolTip(tr("The SMPlayer web server is running"));
tray_menu = new QMenu;
QAction * stopServerAct = new QAction(Images::icon("close"), tr("&Stop the SMPlayer web server"), tray_menu);
connect(stopServerAct, SIGNAL(triggered()), this, SLOT(stopServer()));
tray_menu->addAction(stopServerAct);
tray_icon->setContextMenu(tray_menu);
#endif
}
Chromecast::~Chromecast() {
if (server_process) {
stopServer();
}
saveSettings();
#ifdef CC_USE_TRAY_ICON
delete tray_icon;
delete tray_menu;
#endif
}
void Chromecast::openStream(const QString & url, const QString & title) {
if (output_device == DChromecast) {
QDesktopServices::openUrl(QUrl(URL_CHROMECAST "/?title=" + title.toUtf8().toBase64() +
"&url=" + url.toUtf8().toBase64()));
}
#ifdef USE_QRCODE
else
if (output_device == DMobile) {
OpenWithDeviceDialog d;
d.setQRCodeText(url);
d.exec();
}
#endif
}
void Chromecast::openLocal(const QString & file, const QString & title, const QString & subtitle) {
qDebug() << "Chromecast::openLocal: file:" << file << "subtitle:" << subtitle;
QFileInfo fi(file);
QString dir = fi.absolutePath();
QString filename = fi.fileName();
qDebug() << "Chromecast::openLocal: dir:" << dir;
qDebug() << "Chromecast::openLocal: filename:" << filename;
QString local_address = localAddress();
if (local_address.isEmpty()) local_address = findLocalAddress();
qDebug() << "Chromecast::openLocal: chosen address:" << local_address;
#ifdef SERVE_FILE_DIR_ONLY
QString sub_filename;
if (!subtitle.isEmpty()) {
if (QFile::exists(dir +"/"+ subtitle)) {
sub_filename = subtitle;
qDebug() << "Chromecast::openLocal: sub_filename:" << sub_filename;
} else {
if (QFile::exists(subtitle)) {
QFileInfo sub_fi(subtitle);
if (QFile::exists(dir +"/"+ sub_fi.fileName())) {
sub_filename = sub_fi.fileName();
}
}
}
sub_filename = checkForVTT(dir, sub_filename);
qDebug() << "Chromecast::openLocal: sub_filename:" << sub_filename;
}
#else
QString sub_filepath;
if (!subtitle.isEmpty()) {
if (QFile::exists(subtitle)) {
sub_filepath = subtitle;
} else {
if (QFile::exists(dir +"/"+ subtitle)) {
sub_filepath = dir +"/"+ subtitle;
}
}
qDebug() << "Chromecast::openLocal: sub_filepath:" << sub_filepath;
}
#endif
if (!local_address.isEmpty()) {
#ifdef SERVE_FILE_DIR_ONLY
startServer(dir);
QString url = "http://" + local_address + ":" + QString::number(server_port) + "/" + filename;
QString sub_url;
if (!sub_filename.isEmpty()) {
sub_url = "http://" + local_address + ":" + QString::number(server_port) + "/" + sub_filename;
}
#else
QString root = QDir::rootPath();
startServer(root);
QString abs_filename = filepathWithoutRoot(file);
qDebug() << "Chromecast::openLocal: abs_filename:" << abs_filename;
QString url = "http://" + local_address + ":" + QString::number(server_port) + "/" + abs_filename;
QString abs_sub_filename = filepathWithoutRoot(sub_filepath);
qDebug() << "Chromecast::openLocal: abs_sub_filename:" << abs_sub_filename;
QString sub_url = "http://" + local_address + ":" + QString::number(server_port) + "/" + abs_sub_filename;
#endif
qDebug() << "Chromecast::openLocal: url:" << url;
qDebug() << "Chromecast::openLocal: sub_url:" << sub_url;
#if 1
if (output_device == DChromecast) {
QDesktopServices::openUrl(QUrl(URL_CHROMECAST "/?title=" + title.toUtf8().toBase64() +
"&url=" + url.toUtf8().toBase64() + "&subtitles=" + sub_url.toUtf8().toBase64()));
}
#ifdef USE_QRCODE
else
if (output_device == DMobile) {
OpenWithDeviceDialog d;
QString url = "http://" + local_address + ":" + QString::number(server_port) + "/" + QUrl::toPercentEncoding(filename);
d.setQRCodeText(url);
d.exec();
}
#endif
#endif
}
}
#ifndef SERVE_FILE_DIR_ONLY
QString Chromecast::filepathWithoutRoot(const QString & filepath) {
QString root = QDir::rootPath();
QFileInfo fi(filepath);
QString abs_filename = fi.absoluteFilePath();
if (abs_filename.startsWith(root)) {
abs_filename = abs_filename.mid(root.length());
}
return abs_filename;
}
#endif
QString Chromecast::checkForVTT(const QString & video_path, const QString & subtitle_file) {
QString actual_subtitle = subtitle_file;
QString dir = video_path;
QString subtitle_path = dir +"/"+ subtitle_file;
QFileInfo fi(subtitle_path);
if (fi.suffix().toLower() == "srt") {
qDebug() << "Chromecast::checkForVTT: subtitle is in srt format";
#ifdef CONVERT_TO_VTT
if (autoconvert_to_vtt) {
SubReader sr;
sr.setInputCodec(sub_encoding.toLatin1());
sr.setVTTLinePosition(sub_position);
if (use_sub_filter) {
QString filter = sub_filter;
if (filter.isEmpty()) filter = "opensubtitles|subtitulos\\.es|tusubtitulo|osdb|addic7ed";
sr.setTextFilter(filter);
}
sr.setOverwriteVTT(overwrite_vtt);
sr.autoConvertToVTT(subtitle_path);
}
#endif
QString vtt_subtitle = fi.completeBaseName() + ".vtt";
QString vtt_subtitle_path = dir +"/"+ vtt_subtitle;
if (QFile::exists(vtt_subtitle_path)) {
qDebug() << "Chromecast::checkForVTT: using" << vtt_subtitle;
actual_subtitle = vtt_subtitle;
}
}
return actual_subtitle;
}
QStringList Chromecast::localAddresses() {
QStringList l;
foreach(const QHostAddress &address, QNetworkInterface::allAddresses()) {
qDebug() << "Chromecast::localAddresses:" << address.toString();
if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress(QHostAddress::LocalHost)) {
l << address.toString();
}
}
return l;
}
QString Chromecast::findLocalAddress() {
QString local_address;
QUdpSocket s;
s.connectToHost("8.8.8.8", 53);
if (s.waitForConnected(2000)) {
local_address = s.localAddress().toString();
qDebug() << "Chromecast::findLocalAddress: connected to:" << s.peerAddress().toString() << "local address:" << local_address;
s.disconnectFromHost();
}
if (local_address.isEmpty()) {
QStringList addresses = localAddresses();
qDebug() << "Chromecast::defaultLocalAddress: all IPv4 addresses:" << addresses;
foreach(QString address, addresses) {
if (address.startsWith("192.") && !address.endsWith(".1")) {
local_address = address;
break;
}
}
if (local_address.isEmpty() && !addresses.isEmpty()) local_address = addresses[0];
}
return local_address;
}
void Chromecast::startServer(QString doc_root) {
qDebug("Chromecast::startServer");
static QString last_doc_root;
if (server_process == 0) {
server_process = new QProcess(this);
server_process->setProcessChannelMode( QProcess::MergedChannels );
connect(server_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcessOutput()));
connect(server_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(processFinished(int, QProcess::ExitStatus)));
connect(server_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError)));
}
if (server_process->state() == QProcess::Running) {
if (!server_needs_restart && last_doc_root == doc_root) {
return;
} else {
stopServer();
}
}
QString prog;
QStringList args;
#if defined(Q_OS_WIN) && defined(SERVE_FILE_DIR_ONLY)
doc_root = Helper::shortPathName(doc_root);
#endif
#if defined(CHROMECAST_USE_SERVER_WEBFSD)
prog = "webfsd";
args << "-F" << "-d" << "-4" << "-p" << QString::number(server_port) << "-r" << doc_root;
if (!directoryListing()) args << "-j";
#elif defined(CHROMECAST_USE_SERVER_SIMPLE_WEB_SERVER)
prog = "simple_web_server";
#ifdef Q_OS_WIN
prog += ".exe";
#endif
args << "-p" << QString::number(server_port) << "-r" << doc_root;
if (!directoryListing()) args << "-j";
#else
#error "No server defined"
#endif
{
QString command = prog + " " + args.join(" ");
qDebug() << "Chromecast::startServer: command:" << command;
}
server_process->start(prog, args);
last_doc_root = doc_root;
server_needs_restart = false;
#ifdef CC_USE_TRAY_ICON
tray_icon->show();
#endif
}
void Chromecast::stopServer() {
qDebug("Chromecast::stopServer");
if (server_process) {
server_process->terminate();
if (!server_process->waitForFinished(5000)) {
server_process->kill();
server_process->waitForFinished();
}
}
#ifdef CC_USE_TRAY_ICON
tray_icon->hide();
#endif
}
void Chromecast::readProcessOutput() {
QByteArray line;
while (server_process->canReadLine()) {
line = server_process->readLine().trimmed();
qDebug() << "Chromecast::readProcessOutput: line:" << line;
}
}
void Chromecast::processFinished(int exit_code, QProcess::ExitStatus exit_status) {
qDebug() << "Chromecast::processFinished: exit_code:" << exit_code << "exit_status:" << exit_status;
}
void Chromecast::processError(QProcess::ProcessError error) {
qDebug() << "Chromecast::processError:" << error;
if (error == QProcess::FailedToStart) {
qDebug("Chromecast::processError: process failed to start");
}
}
void Chromecast::loadSettings() {
qDebug("Chromecast::loadSettings");
if (settings) {
settings->beginGroup("chromecast/server");
setServerPort(settings->value("port", serverPort()).toInt());
setLocalAddress(settings->value("local_address", localAddress()).toString());
setDirectoryListing(settings->value("directory_listing", directoryListing()).toBool());
settings->endGroup();
#ifdef CONVERT_TO_VTT
settings->beginGroup("chromecast/subtitles");
setAutoConvertToVTT(settings->value("autoconvert_to_vtt", autoConvertToVTT()).toBool());
setSubtitlePosition(settings->value("position", subtitlePosition()).toInt());
setSubtitleFilter(settings->value("text_filter", subtitleFilter()).toString());
setOverwriteVTT(settings->value("overwrite_vtt", overwriteVTT()).toBool());
enableSubtitleFilter(settings->value("use_sub_filter", isSubtitleFilterEnabled()).toBool());
settings->endGroup();
#endif
}
}
void Chromecast::saveSettings() {
qDebug("Chromecast::saveSettings");
if (settings) {
settings->beginGroup("chromecast/server");
settings->setValue("port", serverPort());
settings->setValue("local_address", localAddress());
settings->setValue("directory_listing", directoryListing());
settings->endGroup();
#ifdef CONVERT_TO_VTT
settings->beginGroup("chromecast/subtitles");
settings->setValue("autoconvert_to_vtt", autoConvertToVTT());
settings->setValue("position", subtitlePosition());
settings->setValue("text_filter", subtitleFilter());
settings->setValue("overwrite_vtt", overwriteVTT());
settings->setValue("use_sub_filter", isSubtitleFilterEnabled());
settings->endGroup();
#endif
}
}
#include "moc_chromecast.cpp"