/* * Copyright (c) 2009 Mark Liversedge (liversedge@gmail.com) * 2015 Vianney Boyer (vlcvboyer@gmail.com) * * 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., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include "VideoWindow.h" #include "Context.h" #include "Athlete.h" #include "RideItem.h" #include "RideFile.h" #include "MeterWidget.h" #include "VideoLayoutParser.h" VideoWindow::VideoWindow(Context *context) : GcChartWindow(context), context(context), m_MediaChanged(false) { QWidget *c = NULL; setProperty("color", QColor(Qt::black)); QHBoxLayout *layout = new QHBoxLayout(); setChartLayout(layout); curPosition = 1; init = true; // assume initialisation was ok ... #ifdef GC_VIDEO_VLC // // USE VLC VIDEOPLAYER // // config parameters to libvlc const char * const vlc_args[] = { "-I", "dummy", /* Don't use any interface */ "--ignore-config", /* Don't use VLC's config */ "--disable-screensaver", /* disable screensaver during playback */ #ifdef Q_OS_LINUX "--no-xlib", // avoid xlib thread error messages #endif //"--verbose=-1", // -1 = no output at all //"--quiet" }; /* Load the VLC engine */ inst = libvlc_new(sizeof(vlc_args) / sizeof(vlc_args[0]), vlc_args); /* Create a new item */ if (inst) { // if vlc doesn't initialise don't even try! m = NULL; /* Create a media player playing environement */ mp = libvlc_media_player_new (inst); container = new QWidget(this); layout->addWidget(container); #if defined(WIN32) libvlc_media_player_set_hwnd (mp, (HWND)(container->winId())); #elif defined(Q_OS_MAC) libvlc_media_player_set_nsobject (mp, (void*)(container->winId())); #elif defined(Q_OS_LINUX) libvlc_media_player_set_xwindow (mp, container->winId()); #endif #if defined(WIN32) || defined(Q_OS_LINUX) // Read the video layouts just to list the names for the layout selector readVideoLayout(-1); // Create the layout selector form c = new QWidget; QVBoxLayout *cl = new QVBoxLayout(c); QFormLayout *controlsLayout = new QFormLayout(); controlsLayout->setSpacing(0); controlsLayout->setContentsMargins(5,5,5,5); cl->addLayout(controlsLayout); QLabel *layoutLabel = new QLabel(tr("Video meters layout"), this); layoutLabel->setAutoFillBackground(true); layoutSelector = new QComboBox(this); for(int i = 0; i < layoutNames.length(); i++) { layoutSelector->addItem(layoutNames[i], i); } controlsLayout->addRow(layoutLabel, layoutSelector); #endif } else { // something went wrong ! init = false; } #endif #ifdef GC_VIDEO_QT5 // USE QT VIDEO PLAYER wd = new QVideoWidget(this); wd->show(); mp = new QMediaPlayer(this); mp->setVideoOutput(wd); layout->addWidget(wd); #endif setControls(c); if (init) { // get updates.. connect(context, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData))); connect(context, SIGNAL(stop()), this, SLOT(stopPlayback())); connect(context, SIGNAL(start()), this, SLOT(startPlayback())); connect(context, SIGNAL(pause()), this, SLOT(pausePlayback())); connect(context, SIGNAL(seek(long)), this, SLOT(seekPlayback(long))); connect(context, SIGNAL(unpause()), this, SLOT(resumePlayback())); connect(context, SIGNAL(mediaSelected(QString)), this, SLOT(mediaSelected(QString))); connect(context, SIGNAL(configChanged(qint32)), this, SLOT(layoutChanged())); connect(layoutSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(layoutChanged())); // The video file may have been already selected mediaSelected(context->videoFilename); // We may add a video player while training is already running! if(context->isRunning) startPlayback(); // Instantiate a layout as initial default layoutChanged(); } } VideoWindow::~VideoWindow() { if (!init) return; // we didn't initialise properly so all bets are off stopPlayback(); #ifdef GC_VIDEO_VLC // VLC /* No need to keep the media now */ if (m) libvlc_media_release (m); /* nor the player */ libvlc_media_player_release (mp); // unload vlc libvlc_release (inst); #endif #ifdef GC_VIDEO_QT5 // QT MEDIA delete mp; delete wd; #endif } void VideoWindow::layoutChanged() { readVideoLayout(videoLayout()); } void VideoWindow::readVideoLayout(int pos) { // Video Overlays Initialization: if video config file is not present // copy a default one to be used as a model by the user. // An empty video-layout.xml file disables video overlays QString filename = context->athlete->home->config().canonicalPath() + "/" + "video-layout.xml"; QFile file(filename); if (!file.exists()) { file.setFileName(":/xml/video-layout.xml"); file.copy(filename); QFile::setPermissions(filename, QFileDevice::ReadUser|QFileDevice::WriteUser); } if (file.exists()) { // clean previous layout foreach(MeterWidget* p_meterWidget, m_metersWidget) { m_metersWidget.removeAll(p_meterWidget); p_meterWidget->deleteLater(); } layoutNames.clear(); VideoLayoutParser handler(&m_metersWidget, &layoutNames, container); QXmlInputSource source(&file); QXmlSimpleReader reader; handler.layoutPositionSelected = pos; reader.setContentHandler(&handler); reader.parse(source); qDebug() << "Video Layout parsing: " << layoutNames; if(context->isRunning) showMeters(); } else { qDebug() << qPrintable(QString("file" + filename + " (video layout XML file) not found")); } } void VideoWindow::showMeters() { foreach(MeterWidget* p_meterWidget , m_metersWidget) { p_meterWidget->setWindowOpacity(1); // Show the widget p_meterWidget->AdjustSizePos(); p_meterWidget->update(); p_meterWidget->raise(); p_meterWidget->show(); } prevPosition = mapToGlobal(pos()); } void VideoWindow::resizeEvent(QResizeEvent * ) { foreach(MeterWidget* p_meterWidget , m_metersWidget) p_meterWidget->AdjustSizePos(); prevPosition = mapToGlobal(pos()); } void VideoWindow::startPlayback() { if ((!context->isRunning) && context->currentVideoSyncFile()) { context->currentVideoSyncFile()->manualOffset = 0.0; context->currentVideoSyncFile()->km = 0.0; } #ifdef GC_VIDEO_VLC if (!m) return; // ignore if no media selected // stop playback & wipe player libvlc_media_player_stop (mp); /* set the media to playback */ libvlc_media_player_set_media (mp, m); /* Reset playback rate */ /* If video speed will be controlled by a sync file, set almost stationary until first telemetry update. Otherwise (re)set to normal rate */ if (context->currentVideoSyncFile() && context->currentVideoSyncFile()->Points.count() > 1) libvlc_media_player_set_rate(mp, 0.1f); else libvlc_media_player_set_rate(mp, 1.0f); /* play the media_player */ libvlc_media_player_play (mp); m_MediaChanged = false; #endif #ifdef GC_VIDEO_QT5 // open the media object mp->play(); #endif showMeters(); } void VideoWindow::stopPlayback() { if ((!context->isRunning) && context->currentVideoSyncFile()) context->currentVideoSyncFile()->manualOffset = 0.0; #ifdef GC_VIDEO_VLC if (!m) return; // ignore if no media selected // stop playback & wipe player libvlc_media_player_stop (mp); #endif #ifdef GC_VIDEO_QT5 mp->stop(); #endif foreach(MeterWidget* p_meterWidget , m_metersWidget) p_meterWidget->hide(); } void VideoWindow::pausePlayback() { #ifdef GC_VIDEO_VLC if (!m) return; // ignore if no media selected // stop playback & wipe player libvlc_media_player_set_pause(mp, true); #endif #ifdef GC_VIDEO_QT5 mp->pause(); #endif } void VideoWindow::resumePlayback() { #ifdef GC_VIDEO_VLC if (!m) return; // ignore if no media selected // stop playback & wipe player if(m_MediaChanged) startPlayback(); else libvlc_media_player_set_pause(mp, false); #endif #ifdef GC_VIDEO_QT5 mp->play(); #endif } void VideoWindow::telemetryUpdate(RealtimeData rtd) { bool metric = context->athlete->useMetricUnits; foreach(MeterWidget* p_meterWidget , m_metersWidget) { if (p_meterWidget->Source() == QString("None")) { //Nothing } else if (p_meterWidget->Source() == QString("Speed")) { p_meterWidget->Value = rtd.getSpeed() * (metric ? 1.0 : MILES_PER_KM); p_meterWidget->Text = QString::number((int)p_meterWidget->Value); p_meterWidget->AltText = QString(".") +QString::number((int)(p_meterWidget->Value * 10.0) - (((int) p_meterWidget->Value) * 10)) + (metric ? tr(" kph") : tr(" mph")); } else if (p_meterWidget->Source() == QString("Elevation")) { // Do not show in ERG mode if (rtd.mode == ERG || rtd.mode == MRC) { p_meterWidget->setWindowOpacity(0); // Hide the widget } p_meterWidget->Value = rtd.getRouteDistance(); ElevationMeterWidget* elevationMeterWidget = dynamic_cast(p_meterWidget); if (!elevationMeterWidget) qDebug() << "Error: Elevation keyword used but widget is not elevation type"; else { elevationMeterWidget->setContext(context); elevationMeterWidget->gradientValue = rtd.getSlope(); } } else if (p_meterWidget->Source() == QString("Cadence")) { p_meterWidget->Value = rtd.getCadence(); p_meterWidget->Text = QString::number((int)p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("Watt")) { p_meterWidget->Value = rtd.getWatts(); p_meterWidget->Text = QString::number((int)p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("HRM")) { p_meterWidget->Value = rtd.getHr(); p_meterWidget->Text = QString::number((int)p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("Load")) { if (rtd.mode == ERG || rtd.mode == MRC) { p_meterWidget->Value = rtd.getLoad(); p_meterWidget->Text = QString("%1").arg(round(p_meterWidget->Value)); p_meterWidget->AltText = tr("w"); } else { p_meterWidget->Value = rtd.getSlope(); p_meterWidget->Text = QString("%1").arg(p_meterWidget->Value, 0, 'f', 1); p_meterWidget->AltText = tr("%"); } } else if (p_meterWidget->Source() == QString("Distance")) { p_meterWidget->Value = rtd.getDistance() * (metric ? 1.0 : MILES_PER_KM); p_meterWidget->Text = QString::number((int) p_meterWidget->Value); p_meterWidget->AltText = QString(".") +QString::number((int)(p_meterWidget->Value * 10.0) - (((int) p_meterWidget->Value) * 10)) + (metric ? tr(" km") : tr(" mi")); } else if (p_meterWidget->Source() == QString("Time")) { p_meterWidget->Value = round(rtd.value(RealtimeData::Time)/100.0)/10.0; p_meterWidget->Text = time_to_string(p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("LapTime")) { p_meterWidget->Value = round(rtd.value(RealtimeData::LapTime)/100.0)/10.0; p_meterWidget->Text = time_to_string(p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("LapTimeRemaining")) { p_meterWidget->Value = round(rtd.value(RealtimeData::LapTimeRemaining)/100.0)/10.0; p_meterWidget->Text = time_to_string(p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("ErgTimeRemaining")) { p_meterWidget->Value = round(rtd.value(RealtimeData::ErgTimeRemaining)/100.0)/10.0; p_meterWidget->Text = time_to_string(p_meterWidget->Value); } else if (p_meterWidget->Source() == QString("TrainerStatus")) { if (!rtd.getTrainerStatusAvailable()) { // we don't have status from trainer thus we cannot indicate anything on screen p_meterWidget->Text = tr(""); } else if (rtd.getTrainerCalibRequired()) { p_meterWidget->setColor(QColor(255,0,0,180)); p_meterWidget->Text = tr("Calibration required"); } else if (rtd.getTrainerConfigRequired()) { p_meterWidget->setColor(QColor(255,0,0,180)); p_meterWidget->Text = tr("Configuration required"); } else if (rtd.getTrainerBrakeFault()) { p_meterWidget->setColor(QColor(255,0,0,180)); p_meterWidget->Text = tr("brake fault"); } else if (rtd.getTrainerReady()) { p_meterWidget->setColor(QColor(0,255,0,180)); p_meterWidget->Text = tr("Ready"); } else { p_meterWidget->Text = tr(""); } } } // The Meter Widgets need to follow the Video Window when it moves // (main window moves, scrolling...), we check the position at every update if(mapToGlobal(pos()) != prevPosition) resizeEvent(NULL); foreach(MeterWidget* p_meterWidget , m_metersWidget) p_meterWidget->update(); #ifdef GC_VIDEO_NONE Q_UNUSED(rtd) #endif #ifdef GC_VIDEO_VLC if (!m || !context->isRunning || context->isPaused) return; // find the curPosition if (context->currentVideoSyncFile()) { // when we selected a videosync file in training mode (rlv...): QVector VideoSyncFiledataPoints = context->currentVideoSyncFile()->Points; if (VideoSyncFiledataPoints.count()<2) return; if (curPosition > VideoSyncFiledataPoints.count() - 1 || curPosition < 1) curPosition = 1; // minimum curPosition is 1 as we will use [curPosition-1] double CurrentDistance = qBound(0.0, rtd.getDistance() + context->currentVideoSyncFile()->manualOffset, context->currentVideoSyncFile()->Distance); context->currentVideoSyncFile()->km = CurrentDistance; // make sure the current position is less than the new distance while ((VideoSyncFiledataPoints[curPosition].km > CurrentDistance) && (curPosition > 1)) curPosition--; while ((VideoSyncFiledataPoints[curPosition].km <= CurrentDistance) && (curPosition < VideoSyncFiledataPoints.count()-1)) curPosition++; /* Create an RFP to represent where we are */ VideoSyncFilePoint syncPrevious = VideoSyncFiledataPoints[curPosition-1]; VideoSyncFilePoint syncNext = VideoSyncFiledataPoints[curPosition]; double syncKmDelta = syncNext.km - syncPrevious.km; double syncKphDelta = syncNext.kph - syncPrevious.kph; double syncTimeDelta = syncNext.secs - syncPrevious.secs; double distanceFactor, speedFactor, timeFactor, timeExtra; // Calculate how far we are between points in terms of distance if (syncKmDelta == 0) distanceFactor = 0.0; else distanceFactor = (CurrentDistance - syncPrevious.km) / syncKmDelta; // Now create the appropriate factors and interpolate the // video speed and time for the point we have reached. // If there has been no acceleration we can just use use the distance factor if (syncKphDelta == 0) { // Constant filming speed rfp.kph = syncPrevious.kph; rfp.secs = syncPrevious.secs + syncTimeDelta * distanceFactor; } else { // Calculate time difference because of change in speed timeExtra = syncTimeDelta - ((syncKmDelta / syncPrevious.kph) * 3600); if (syncKphDelta > 0) { // The filming speed increased speedFactor = qPow(distanceFactor, 0.66667); timeFactor = qPow(distanceFactor, 0.33333); rfp.kph = syncPrevious.kph + speedFactor * syncKphDelta; } else { // The filming speed decreased speedFactor = 1 - qPow(distanceFactor, 1.5); timeFactor = qPow(distanceFactor, 3.0); rfp.kph = syncNext.kph - speedFactor * syncKphDelta; } rfp.secs = syncPrevious.secs + (distanceFactor * (syncTimeDelta - timeExtra)) + (timeFactor * timeExtra); } rfp.km = CurrentDistance; /* //TODO : GPX file format // otherwise we use the gpx from selected ride in analysis view: QVector dataPoints = myRideItem->ride()->dataPoints(); if (dataPoints.count()<2) return; if(curPosition > dataPoints.count()-1 || curPosition < 1) curPosition = 1; // minimum curPosition is 1 as we will use [curPosition-1] // make sure the current position is less than the new distance while ((dataPoints[curPosition]->km > rtd.getDistance()) && (curPosition > 1)) curPosition--; while ((dataPoints[curPosition]->km <= rtd.getDistance()) && (curPosition < dataPoints.count()-1)) curPosition++; // update the rfp rfp = *dataPoints[curPosition]; } */ // set video rate ( theoretical : video rate = training speed / ghost speed) float rate; float video_time_shift_ms; video_time_shift_ms = (rfp.secs*1000.0 - (double) libvlc_media_player_get_time(mp)); if (rfp.kph == 0.0) rate = 1.0; else rate = rtd.getSpeed() / rfp.kph; //if video is far (empiric) from ghost: if (fabs(video_time_shift_ms) > 5000) { libvlc_media_player_set_time(mp, (libvlc_time_t) (rfp.secs*1000.0)); } else // otherwise add "small" empiric corrective parameter to get video back to ghost position: rate *= 1.0 + (video_time_shift_ms / 10000.0); libvlc_media_player_set_pause(mp, (rate < 0.01)); // change video rate but only if there is a significant change if ((rate != 0.0) && (fabs((rate - currentVideoRate) / rate) > 0.05)) { libvlc_media_player_set_rate(mp, rate ); currentVideoRate = rate; } } #endif #ifdef GC_VIDEO_QT5 //TODO // // seek to ms position in current file // mp->setPosition(ms); #endif } void VideoWindow::seekPlayback(long ms) { #ifdef GC_VIDEO_NONE Q_UNUSED(ms) #endif #ifdef GC_VIDEO_VLC if (!m) return; // when we selected a videosync file in training mode (rlv...) if (context->currentVideoSyncFile()) { context->currentVideoSyncFile()->manualOffset += (double) ms; //we consider +/- 1km } else { // seek to ms position in current file libvlc_media_player_set_time(mp, (libvlc_time_t) ms); } #endif #ifdef GC_VIDEO_QT5 mp->setPosition(ms); #endif } void VideoWindow::mediaSelected(QString filename) { #ifdef GC_VIDEO_NONE Q_UNUSED(filename); #endif #ifdef GC_VIDEO_VLC // VLC // stop any current playback stopPlayback(); // release whatever is already loaded if (m) libvlc_media_release(m); m = NULL; if (filename.endsWith("/DVD") || (filename != "" && QFile(filename).exists())) { // properly encode the filename as URL (with all special characters) filename = QUrl::toPercentEncoding(filename, "/\\", ""); #ifdef Q_OS_LINUX QString fileURL = "file://" + filename.replace("\\", "/"); #else // A Windows "c:\xyz\abc def.avi" filename should become file:///c:/xyz/abc%20def.avi QString fileURL = "file:///" + filename.replace("\\", "/"); #endif //qDebug()<<"file url="<setMedia(mc); #endif if(context->isRunning) startPlayback(); } MediaHelper::MediaHelper() { // construct a list of supported types // Using the basic list from the VLC // Wiki here: http://www.videolan.org/vlc/features.html and then looked for // the common extensions used from here: http://www.fileinfo.com/filetypes/video supported << ".3GP"; supported << ".ASF"; supported << ".AVI"; supported << ".DIVX"; supported << ".FLV"; supported << ".M4V"; supported << ".MKV"; supported << ".MOV"; supported << ".MP4"; supported << ".MPEG"; supported << ".MPG"; supported << ".MXF"; supported << ".VOB"; supported << ".WMV"; } MediaHelper::~MediaHelper() { } QStringList MediaHelper::listMedia(QDir dir) { QStringList returning; // go through the sub directories QDirIterator directory_walker(dir, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); while(directory_walker.hasNext()){ directory_walker.next(); // whizz through every file in the directory // if it has the right extension then we are happy QString name = directory_walker.filePath(); foreach(QString extension, supported) { if (name.endsWith(extension, Qt::CaseInsensitive)) { name.remove(dir.absolutePath()); if(name.startsWith('/') || name.startsWith('\\')) // remove '/' (linux/mac) or '\' (windows?) name.remove(0,1); returning << name; break; } } } return returning; } bool MediaHelper::isMedia(QString name) { foreach (QString extension, supported) { if (name.endsWith(extension, Qt::CaseInsensitive)) return true; } return false; }