diff --git a/TODO b/TODO index fbffd99b..1a40dc32 100644 --- a/TODO +++ b/TODO @@ -1,10 +1,10 @@ [Book] -- Make the feature +- Make the feature? [Cover] -- Handle preferred cover file names -- Implement a cache and a grabber from some web service (mandatory for artists) +- Handle preferred cover file names ("front.xxx", "cover.xxx", ...) +- Implement a cache and a grabber from some web service (mandatory for artists?) [Database] - Optim, use SQL query to get the "genre" orphans @@ -13,7 +13,6 @@ - add a global play counter for tracks. This will help people to spot most popular files - rework the exception process in av/metadata/updater in case of bad files - Use the WServer::post method to notify the end of the database scan? (with results?) -- Put more config information in the database, access from the UI [Metadata] - OGG metadata -> properly handle metadata nested in the audio stream @@ -33,7 +32,7 @@ - Handle internationalization - Handle internal path [Settings] - - logout users that are being changed (loss of admin admin rights), or make sure they are still admin when they make changes + - logout users that are being changed (loss of admin rights), or make sure they are still admin when they make changes - "signal not exposed" problem if a user logout and login again. Bad resource destruction? - add a scroll area in order for settings to work on mobile devices [user/transcoding] @@ -51,20 +50,19 @@ - Add a download button to get the current playlist in a streamed zip file - Add a upload button to upload media files in a dedicated directory - Perform a search for each key pressed (using a timer, in JS) + - Save user prefs in local storage (volume, loop, shuffle, current playqueue, etc.) [desktop] - Better style... - - MediaPlayer: move the slider using js (http://redmine.webtoolkit.eu/boards/2/topics/7924?r=8478, http://redmine.emweb.be/boards/2/topics/10994) - TrackView: Reselect the current selected item when displaying the updated search results - Add keyboard shortcuts [mobile] + - Reorganize the views to ease internal paths - Covers not always the same height - 'duration' and 'play' tags are broken if title is too long - - Remove the extra search widget - Cover loading breaks the vertical alignment - Implement a play queue - Implement a decent player - - Make the release clickable in the Track result view - - Reorganize the views to ease internal paths? + - Make the release/artist clickable when displayed to the user [Video] - implement a decent mediaplayer diff --git a/approot/templates.xml b/approot/templates.xml index b7924f8d..c6e1c600 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -529,7 +529,23 @@ - ${shuffle} ${repeat} ${playlist} ${prev} ${play-pause} ${next} ${cover} ${artist} ${track} ${release} ${curtime} ${seekbar} ${duration} ${volume} + ${audio} + ${prev}${play-pause}${next} +
+ ${volume} +
+ +
+
+ ${track} ${artist} +
+
+ ${curtime}${seekbar}${duration} +
+
+ ${shuffle}${repeat}${playlist}
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index afe63087..6c0bf27d 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -1,5 +1,6 @@ .main-nav { margin-bottom: 0px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .Wt-hrh2 { @@ -10,14 +11,10 @@ background-color: white; } -div.contents { - padding: 0px 12px 6px; - margin-bottom: 64px; -} - .playqueue { background-color: #EEE; border-radius: 10px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } .playqueue-playing { @@ -40,32 +37,98 @@ div.contents { height: 64px; } -.mediaplayer { - background-color: #CCC; - border-radius: 10px; - min-width: 360px; -} - -.mediaplayer-btn-controls { - font-weight: bold; -} - -.mediaplayer-current-cover { - width: 72px; - height: 72px; - border-radius: 8px; - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); -} - -.mediaplayer-current-track { +.mediaplayer-track { font-weight: bold; font-size: 120%; } -.mediaplayer-current-artist { +.mediaplayer-artist { font-style: italic; } +.mediaplayer { + background-color: #CCC; + border-radius: 10px; + display: flex; + justify-content: space-between; + align-items: center; + height: 72px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); +} + +.mediaplayer-cover { + margin: 8px; + width: 64px; + min-width: 64px; + height: 64px; + min-height: 64px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.mediaplayer-cover img { + border-radius: 8px; + border-radius: 8px; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); +} + +.mediaplayer-btn { + margin: 8px; +} + +.mediaplayer-btn i { + cursor: pointer; + text-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); +} + +.mediaplayer-btn-active { + color: #428bca; +} + +.mediaplayer-info-container { + width: 100%; + min-width: 100px; + margin: 8px; +} + +.mediaplayer-track-info { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.mediaplayer-progress-container { + display: flex; + align-items: center; +} + +.mediaplayer-badge { + min-width: initial; +} + +.mediaplayer-current-duration { + margin-right: 8px; +} +.mediaplayer-total-duration { + margin-left: 8px; +} + +.mediaplayer-seekbar { +} + +.mediaplayer-volume-container { + width: 24px; + height: 64px; + margin: 8px; +} + +.mediaplayer-volume { + height: 64px; + -webkit-appearance: slider-vertical; + writing-mode: bt-lr; +} + .mobile-search-title { font-weight: bold; height: 32px; diff --git a/src/Makefile.am b/src/Makefile.am index 7cc1077c..e6b0f919 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,7 +26,6 @@ lms_SOURCES = \ $(srcdir)/ui/auth/LmsAuth.cpp \ $(srcdir)/ui/audio/AudioPlayer.cpp \ $(srcdir)/ui/audio/desktop/DesktopAudio.cpp \ - $(srcdir)/ui/audio/desktop/AudioMediaPlayer.cpp \ $(srcdir)/ui/audio/desktop/FilterChain.cpp \ $(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \ $(srcdir)/ui/audio/desktop/PlayQueue.cpp \ diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp index 5706ea94..7e671fb8 100644 --- a/src/av/AvTranscoder.hpp +++ b/src/av/AvTranscoder.hpp @@ -91,6 +91,8 @@ class Transcoder void process(std::vector& output, std::size_t maxSize); bool isComplete(void) { return _isComplete; } + const TranscodeParameters& getParameters() const { return _parameters; } + private: Transcoder(); diff --git a/src/ui/audio/AudioPlayer.cpp b/src/ui/audio/AudioPlayer.cpp index f529dc78..7df769c1 100644 --- a/src/ui/audio/AudioPlayer.cpp +++ b/src/ui/audio/AudioPlayer.cpp @@ -33,21 +33,49 @@ namespace UserInterface { -void + +Av::Encoding +AudioPlayer::getBestEncoding() const +{ + // TODO get the supported formats of the player and pick one + return Av::Encoding::MP3; +} + + +bool AudioPlayer::loadTrack(Database::Track::id_type trackId) { + // TODO reduce this scope Wt::Dbo::Transaction transaction(DboSession()); Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); if (!track) { LMS_LOG(UI, INFO) << "No track found for id " << trackId; - return; + return false; } - _trackName->setText(Wt::WString::fromUTF8(track->getName())); - _artistName->setText( Wt::WString::fromUTF8(track->getArtist()->getName())); - _releaseName->setText( Wt::WString::fromUTF8(track->getRelease()->getName())); + Database::User::pointer user = CurrentUser(); + if (!user) + { + LMS_LOG(UI, ERROR) << "No user found!"; + return false; + } + + Av::Encoding encoding; + switch (user->getAudioEncoding()) + { + case Database::AudioEncoding::MP3: encoding = Av::Encoding::MP3; break; + case Database::AudioEncoding::OGA: encoding = Av::Encoding::OGA; break; + case Database::AudioEncoding::WEBMA: encoding = Av::Encoding::WEBMA; break; + case Database::AudioEncoding::AUTO: + default: + encoding = getBestEncoding(); + break; + } + + bindString("track", Wt::WString::fromUTF8(track->getName())); + bindString("artist", Wt::WString::fromUTF8(track->getArtist()->getName())); _cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64)); _trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() )); @@ -57,7 +85,7 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) if (!mediaFile.open() || !mediaFile.scan()) { LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath(); - return; + return false; } @@ -76,123 +104,184 @@ AudioPlayer::loadTrack(Database::Track::id_type trackId) document.lms.audio.curTime = 0;\ "); + LMS_LOG(UI, DEBUG) << "Loading, URL = '" << SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams) << "'"; + //TODO, try to load everything in JS in order to prevent the WriteError bug? _audio->pause(); _audio->clearSources(); - //TODOencoding - _audio->addSource(SessionTranscodeResource()->getUrl(trackId, Av::Encoding::MP3, 0, streams)); + //TODO, encoding + _audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams)); + _audio->setPreloadMode(Wt::WAudio::PreloadAuto); _audio->play(); + + return true; } AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) -: Wt::WContainerWidget(parent) +: Wt::WTemplate(parent) { - Wt::WTemplate *t = new Wt::WTemplate(this); - t->setTemplateText(Wt::WString::tr("wa-audio-player")); + setTemplateText(Wt::WString::tr("wa-audio-player")); + addStyleClass("mediaplayer"); - _audio = new Wt::WAudio(this); + // TODO potential leak here + _audio = new Wt::WAudio(); _audio->setOptions(Wt::WAudio::Autoplay); _audio->setPreloadMode(Wt::WAudio::PreloadAuto); + bindWidget("audio", _audio); + + _audio->ended().connect(std::bind([=] () + { + _playbackEnded.emit(); + })); _cover = new Wt::WImage(); - t->bindWidget("cover", _cover); + bindWidget("cover", _cover); _cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64)); InputRange *seekbar = new InputRange(); - t->bindWidget("seekbar", seekbar); + seekbar->addStyleClass("mediaplayer-seekbar"); + bindWidget("seekbar", seekbar); - _trackName = new Wt::WText(); - t->bindWidget("track", _trackName); - - _artistName = new Wt::WText(); - t->bindWidget("artist", _artistName); - - _releaseName = new Wt::WText(); - t->bindWidget("release", _releaseName); + bindString("track", "Track"); + bindString("artist", "Artist"); InputRange *volumeSlider = new InputRange(); - t->bindWidget("volume", volumeSlider); + volumeSlider->addStyleClass("mediaplayer-volume"); + volumeSlider->setAttributeValue("orient", "vertical"); // firefox + bindWidget("volume", volumeSlider); - Wt::WPushButton *playlistBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("playlist", playlistBtn); + Wt::WText *playlistBtn = new Wt::WText("", Wt::XHTMLText); + playlistBtn->addStyleClass("mediaplayer-btn"); + bindWidget("playlist", playlistBtn); - Wt::WPushButton *repeatBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("repeat", repeatBtn); + Wt::WText *repeatBtn = new Wt::WText("", Wt::XHTMLText); + repeatBtn->addStyleClass("mediaplayer-btn"); + bindWidget("repeat", repeatBtn); + repeatBtn->clicked().connect(std::bind([=] () + { + if (repeatBtn->hasStyleClass("mediaplayer-btn-active")) + { + repeatBtn->removeStyleClass("mediaplayer-btn-active"); + _loop.emit(false); + } + else + { + repeatBtn->addStyleClass("mediaplayer-btn-active"); + _loop.emit(true); + } + })); - Wt::WPushButton *shuffleBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("shuffle", shuffleBtn); + Wt::WText *shuffleBtn = new Wt::WText("", Wt::XHTMLText); + shuffleBtn->addStyleClass("mediaplayer-btn"); + bindWidget("shuffle", shuffleBtn); + shuffleBtn->clicked().connect(std::bind([=] () + { + if (shuffleBtn->hasStyleClass("mediaplayer-btn-active")) + { + shuffleBtn->removeStyleClass("mediaplayer-btn-active"); + _shuffle.emit(false); + } + else + { + shuffleBtn->addStyleClass("mediaplayer-btn-active"); + _shuffle.emit(true); + } + })); - Wt::WPushButton *prevBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("prev", prevBtn); + Wt::WText *prevBtn = new Wt::WText("", Wt::XHTMLText); + prevBtn->addStyleClass("mediaplayer-btn hidden-xs"); + bindWidget("prev", prevBtn); + prevBtn->clicked().connect(std::bind([=] () + { + _playPrevious.emit(); + })); - Wt::WPushButton *nextBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("next", nextBtn); + Wt::WText *nextBtn = new Wt::WText("", Wt::XHTMLText); + nextBtn->addStyleClass("mediaplayer-btn"); + bindWidget("next", nextBtn); + nextBtn->clicked().connect(std::bind([=] () + { + _playNext.emit(); + })); - Wt::WPushButton *playPauseBtn = new Wt::WPushButton("", Wt::XHTMLText); - t->bindWidget("play-pause", playPauseBtn); + Wt::WText *playPauseBtn = new Wt::WText("", Wt::XHTMLText); + playPauseBtn->addStyleClass("mediaplayer-btn"); + bindWidget("play-pause", playPauseBtn); Wt::WText *trackCurrentTime = new Wt::WText("00:00"); - t->bindWidget("curtime", trackCurrentTime); + trackCurrentTime->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-current-duration"); + bindWidget("curtime", trackCurrentTime); _trackDuration = new Wt::WText("00:00"); - t->bindWidget("duration", _trackDuration); + _trackDuration->addStyleClass("hidden-xs badge mediaplayer-badge mediaplayer-total-duration"); + bindWidget("duration", _trackDuration); this->doJavaScript( - "\ - document.lms = {};\ - document.lms.audio = {};\ - document.lms.audio.audio = " + _audio->jsRef() + ";\ - document.lms.audio.seekbar = " + seekbar->jsRef() +";\ - document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\ - document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\ - document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\ - \ - document.lms.audio.offset = 0;\ - document.lms.audio.curTime = 0;\ - document.lms.audio.state = \"init\";\ - document.lms.audio.volume = 1;\ - \ - document.lms.audio.seekbar.value = 0;\ - document.lms.audio.seekbar.disabled = true;\ - \ - document.lms.audio.volumeSlider.min = 0;\ - document.lms.audio.volumeSlider.max = 100;\ - document.lms.audio.volumeSlider.value = 100;\ - \ - function updateUI() {\ - document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\ - document.lms.audio.seekbar.value = document.lms.audio.curTime;\ - }\ + "\ + document.lms = {};\ + document.lms.audio = {};\ + document.lms.audio.audio = " + _audio->jsRef() + ";\ + document.lms.audio.seekbar = " + seekbar->jsRef() +";\ + document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\ + document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\ + document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\ + \ + document.lms.audio.offset = 0;\ + document.lms.audio.curTime = 0;\ + document.lms.audio.state = \"init\";\ + document.lms.audio.volume = 1;\ + \ + document.lms.audio.seekbar.value = 0;\ + document.lms.audio.seekbar.disabled = true;\ + \ + document.lms.audio.volumeSlider.min = 0;\ + document.lms.audio.volumeSlider.max = 100;\ + document.lms.audio.volumeSlider.value = 100;\ + \ + function updateUIPlaying() { \ + var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \ + icon.className = \"fa fa-pause fa-3x fa-fw\"; \ + } \ + \ + function updateUIStopped() { \ + var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \ + icon.className = \"fa fa-play fa-3x fa-fw\"; \ + } \ + \ + function updateUI() {\ + document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\ + document.lms.audio.seekbar.value = document.lms.audio.curTime;\ + }\ \ var mouseDown = 0;\ function seekMouseDown(e) {\ ++mouseDown;\ }\ - function seekMouseUp(e) {\ - --mouseDown;\ - }\ + function seekMouseUp(e) {\ + --mouseDown;\ + }\ \ function seeking(e) {\ if (document.lms.audio.state == \"init\")\ return;\ - \ - document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\ + \ + document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\ }\ \ function seek(e) {\ if (document.lms.audio.state == \"init\")\ return;\ - \ - document.lms.audio.audio.pause(); \ - document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\ - document.lms.audio.curTime = document.lms.audio.seekbar.value;\ - var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\ - var src = audioSource.src;\ - src = src.slice(0, src.lastIndexOf(\"=\") + 1);\ - audioSource.src = src + document.lms.audio.seekbar.value;\ - document.lms.audio.audio.load(); \ - document.lms.audio.audio.play(); \ - document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\ + \ + document.lms.audio.audio.pause(); \ + document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\ + document.lms.audio.curTime = document.lms.audio.seekbar.value;\ + var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\ + var src = audioSource.src;\ + src = src.slice(0, src.lastIndexOf(\"=\") + 1);\ + audioSource.src = src + document.lms.audio.seekbar.value;\ + document.lms.audio.audio.load(); \ + document.lms.audio.audio.play(); \ + document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\ }\ \ function volumeChanged() {\ @@ -205,26 +294,30 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent) updateUI();\ } \ \ - function playPause() {\ + function playPause() { \ if (document.lms.audio.state == \"init\") \ return;\ - \ - if (document.lms.audio.audio.paused)\ - document.lms.audio.audio.play();\ - else\ - document.lms.audio.audio.pause();\ - \ + \ + if (document.lms.audio.audio.paused) \ + document.lms.audio.audio.play(); \ + else \ + document.lms.audio.audio.pause();\ + \ }\ \ document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \ + document.lms.audio.audio.addEventListener('playing', updateUIPlaying); \ + document.lms.audio.audio.addEventListener('play', updateUIPlaying); \ + document.lms.audio.audio.addEventListener('pause', updateUIStopped); \ + document.lms.audio.audio.addEventListener('ended', updateUIStopped); \ document.lms.audio.seekbar.addEventListener('change', seek);\ document.lms.audio.seekbar.addEventListener('input', seeking);\ document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\ document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\ document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\ document.lms.audio.playPause.addEventListener('click', playPause);\ - " - ); + " + ); } diff --git a/src/ui/audio/AudioPlayer.hpp b/src/ui/audio/AudioPlayer.hpp index f202e93e..0527f2fa 100644 --- a/src/ui/audio/AudioPlayer.hpp +++ b/src/ui/audio/AudioPlayer.hpp @@ -19,28 +19,43 @@ #pragma once +#include +#include + #include "common/InputRange.hpp" #include "database/Types.hpp" +#include "av/AvTranscoder.hpp" namespace UserInterface { -class AudioPlayer : public Wt::WContainerWidget +class AudioPlayer : public Wt::WTemplate { public: AudioPlayer(Wt::WContainerWidget *parent = 0); - void loadTrack(Database::Track::id_type trackId); + Av::Encoding getBestEncoding() const; + bool loadTrack(Database::Track::id_type trackId); - // Slots + // Signals + Wt::Signal& playbackEnded() {return _playbackEnded;} + Wt::Signal& playNext() {return _playNext;} + Wt::Signal& playPrevious() {return _playPrevious;} + Wt::Signal& shuffle() {return _shuffle;} + Wt::Signal& loop() {return _loop;} private: + // Signals + Wt::Signal _playbackEnded; + Wt::Signal _playNext; + Wt::Signal _playPrevious; + Wt::Signal _shuffle; + Wt::Signal _loop; + Wt::WAudio* _audio; Wt::WText* _trackDuration; Wt::WText* _trackName; - Wt::WText* _artistName; - Wt::WText* _releaseName; Wt::WImage* _cover; }; diff --git a/src/ui/audio/desktop/AudioMediaPlayer.cpp b/src/ui/audio/desktop/AudioMediaPlayer.cpp deleted file mode 100644 index 42d9e6cb..00000000 --- a/src/ui/audio/desktop/AudioMediaPlayer.cpp +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS 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 3 of the License, or - * (at your option) any later version. - * - * LMS 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 LMS. If not, see . - */ - -#include -#include -#include -#include -#include - -#include "logger/Logger.hpp" - -#include "LmsApplication.hpp" - -#include "AudioMediaPlayer.hpp" - -namespace UserInterface { -namespace Desktop { - -Wt::WMediaPlayer::Encoding -AudioMediaPlayer::getBestEncoding() -{ - // MP3 seems to be better supported everywhere - return Wt::WMediaPlayer::MP3; -} - -AudioMediaPlayer::AudioMediaPlayer(Wt::WContainerWidget *parent) - : Wt::WContainerWidget(parent), - _mediaResource(nullptr) -{ - this->setStyleClass("mediaplayer"); - - Wt::WVBoxLayout* mainLayout = new Wt::WVBoxLayout(); - this->setLayout(mainLayout); - - // Determine the encoding to be used - { - Wt::Dbo::Transaction transaction(DboSession()); - switch (CurrentUser()->getAudioEncoding()) - { - case Database::AudioEncoding::MP3: _encoding = Wt::WMediaPlayer::MP3; break; - case Database::AudioEncoding::WEBMA: _encoding = Wt::WMediaPlayer::WEBMA; break; - case Database::AudioEncoding::OGA: _encoding = Wt::WMediaPlayer::OGA; break; - case Database::AudioEncoding::AUTO: - default: - _encoding = getBestEncoding(); - } - } - - LMS_LOG(UI, INFO) << "Audio player using encoding " << _encoding; - - // Current Media info - Wt::WHBoxLayout *currentMediaLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(currentMediaLayout, 1); - - currentMediaLayout->addWidget( _mediaCover = new Wt::WImage()); - _mediaCover->setImageLink( LmsApplication::instance()->getCoverResource()->getUnknownTrackUrl(72)); - _mediaCover->setStyleClass("mediaplayer-current-cover"); - - Wt::WVBoxLayout* mediaInfoLayout = new Wt::WVBoxLayout(); - currentMediaLayout->addLayout(mediaInfoLayout, 1); - - mediaInfoLayout->addWidget( _mediaTitle = new Wt::WText("---")); - mediaInfoLayout->addWidget( _mediaArtistRelease = new Wt::WText("---")); - _mediaTitle->setStyleClass("mediaplayer-current-track vertical-align"); - _mediaArtistRelease->setStyleClass("mediaplayer-current-artist vertical-align"); - - // Time control - Wt::WHBoxLayout *sliderLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(sliderLayout); - - sliderLayout->addWidget(_curTime = new Wt::WText("00:00:00")); - sliderLayout->addWidget(_timeSlider = new Wt::WSlider( ), 1); - sliderLayout->addWidget(_duration = new Wt::WText("00:00:00")); - _timeSlider->setHeight(26); // Default is too big (50) - _curTime->setStyleClass("vertical-align"); - _duration->setStyleClass("vertical-align"); - - // Controls - Wt::WHBoxLayout *controlsLayout = new Wt::WHBoxLayout(); - mainLayout->addLayout(controlsLayout); - - - Wt::WPushButton *prevBtn = new Wt::WPushButton("<<"); - controlsLayout->addWidget(prevBtn); - prevBtn->setStyleClass("mediaplayer-btn-controls"); - - Wt::WContainerWidget *btnContainer = new Wt::WContainerWidget();; - - _playBtn = new Wt::WPushButton("Play"); - btnContainer->addWidget(_playBtn); - _playBtn->setWidth(70); - _playBtn->setStyleClass("mediaplayer-btn-controls"); - - _pauseBtn = new Wt::WPushButton("Pause"); - btnContainer->addWidget(_pauseBtn); - _pauseBtn->setWidth(70); - _pauseBtn->setStyleClass("mediaplayer-btn-controls"); - - controlsLayout->addWidget(btnContainer); - - Wt::WPushButton *nextBtn = new Wt::WPushButton(">>"); - controlsLayout->addWidget(nextBtn); - nextBtn->setStyleClass("mediaplayer-btn-controls"); - - _volumeSlider = new Wt::WSlider(); - _volumeSlider->setRange(0,100); - _volumeSlider->setWidth(60); // Default is too big (150) - _volumeSlider->setHeight(26); // Default is too big (50) - _volumeSlider->setMinimumSize(50, Wt::WLength::Auto); - controlsLayout->addWidget(_volumeSlider, 1); - - Wt::WPushButton *loop = new Wt::WPushButton("Loop"); - loop->setCheckable(true); - loop->setStyleClass("btn-xs"); - loop->checked().connect(std::bind([=] () { _loop.emit( true ); })); - loop->unChecked().connect(std::bind([=] () { _loop.emit( false ); })); - controlsLayout->addWidget(loop); - - Wt::WPushButton *shuffle = new Wt::WPushButton("Shuffle"); - shuffle->setCheckable(true); - shuffle->setStyleClass("btn-xs"); - shuffle->checked().connect(std::bind([=] () { _shuffle.emit( true ); })); - shuffle->unChecked().connect(std::bind([=] () { _shuffle.emit( false );})); - controlsLayout->addWidget(shuffle); - - _mediaPlayer = new Wt::WMediaPlayer( Wt::WMediaPlayer::Audio, btnContainer ); - _mediaPlayer->addSource( _encoding, "" ); - _mediaPlayer->ended().connect(this, &AudioMediaPlayer::handleTrackEnded); - - _mediaPlayer->setControlsWidget( 0 ); - _mediaPlayer->setButton(Wt::WMediaPlayer::Play, _playBtn); - _mediaPlayer->setButton(Wt::WMediaPlayer::Pause, _pauseBtn); - - _mediaPlayer->timeUpdated().connect(this, &AudioMediaPlayer::handleTimeUpdated); - - _volumeSlider->setValue(_mediaPlayer->volume() * 100); - - nextBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playNext.emit(); - })); - - prevBtn->clicked().connect(std::bind([=] () - { - _mediaPlayer->stop(); - _playPrevious.emit(); - })); - _timeSlider->valueChanged().connect(this, &AudioMediaPlayer::handlePlayOffset); - _timeSlider->setDisabled(true); - - _volumeSlider->sliderMoved().connect(this, &AudioMediaPlayer::handleVolumeSliderMoved); - -} - -void -AudioMediaPlayer::loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters) -{ - _currentFile = filePath; - _currentParameters = parameters; - - _mediaPlayer->clearSources(); - - if (_mediaResource) - delete _mediaResource; - - _mediaResource = new AvConvTranscodeStreamResource( filePath, parameters, this ); - - _mediaPlayer->addSource( getEncoding(), Wt::WLink(_mediaResource)); - - // Auto play - _mediaPlayer->play(); -} - -bool -AudioMediaPlayer::load(Database::Track::id_type trackId) -{ - Av::TranscodeParameters parameters; - - boost::filesystem::path path; - - boost::posix_time::time_duration duration; - - { - Wt::Dbo::Transaction transaction(DboSession()); - - Database::Track::pointer track = Database::Track::getById(DboSession(), trackId); - if (!track) - { - LMS_LOG(UI, INFO) << "Cannot find track id " << trackId; - return false; - } - - path = track->getPath(); - parameters.setBitrate(Av::Stream::Type::Audio, CurrentUser()->getAudioBitrate() ); - - duration = track->getDuration(); - - _mediaTitle->setText ( Wt::WString::fromUTF8(track->getName()) ); - _mediaArtistRelease->setText ( Wt::WString::fromUTF8(track->getArtist()->getName()) + " - " + Wt::WString::fromUTF8(track->getRelease()->getName()) ); - _mediaCover->setImageLink( Wt::WLink (LmsApplication::instance()->getCoverResource()->getTrackUrl(trackId, 72))); - } - - Av::MediaFile mediaFile(path); - - if (!mediaFile.open() || !mediaFile.scan()) - { - LMS_LOG(UI, INFO) << "Cannot open file '" << path << "'"; - return false; - } - - // It seems to be far better to manually map the streams - // otherwise, some files may have to be fully transcoded to be played by browser... - int audioBestStreamId = mediaFile.getBestStreamId(Av::Stream::Type::Audio); - if (audioBestStreamId != -1) - parameters.addStream(audioBestStreamId); - - Av::Encoding encoding; - switch (_encoding) - { - case Wt::WMediaPlayer::MP3: encoding = Av::Encoding::MP3; break; - case Wt::WMediaPlayer::OGA: encoding = Av::Encoding::OGA; break; - case Wt::WMediaPlayer::WEBMA: encoding = Av::Encoding::WEBMA; break; - default: - encoding = Av::Encoding::MP3; - } - parameters.setEncoding(encoding); - - _timeSlider->setDisabled(false); - _timeSlider->setRange(0, duration.total_seconds() ); - _timeSlider->setValue(0); - - _duration->setText( boost::posix_time::to_simple_string( duration )); - - loadPlayer(path, parameters); - - return true; -} - -void -AudioMediaPlayer::handlePlayOffset(int offsetSecs) -{ - Av::TranscodeParameters parameters = _currentParameters; - - parameters.setOffset( boost::posix_time::seconds(offsetSecs) ); - - loadPlayer(_currentFile, parameters); -} - -void -AudioMediaPlayer::handleTrackEnded(void) -{ - LMS_LOG(UI, INFO) << "Track ended!"; - _playbackEnded.emit(); -} - -void -AudioMediaPlayer::handleTimeUpdated(void) -{ - boost::posix_time::time_duration currentTime ( boost::posix_time::seconds( _mediaPlayer->currentTime() + _currentParameters.getOffset().total_seconds())); - - _timeSlider->setValue( currentTime.total_seconds() ); - _curTime->setText( boost::posix_time::to_simple_string( currentTime) ); -} - -void -AudioMediaPlayer::handleVolumeSliderMoved(int value) -{ - _mediaPlayer->setVolume( value / 100. ); -} - -} // namespace Desktop -} // namespace UserInterface diff --git a/src/ui/audio/desktop/AudioMediaPlayer.hpp b/src/ui/audio/desktop/AudioMediaPlayer.hpp deleted file mode 100644 index 4c3b4416..00000000 --- a/src/ui/audio/desktop/AudioMediaPlayer.hpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS 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 3 of the License, or - * (at your option) any later version. - * - * LMS 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 LMS. If not, see . - */ - -#ifndef __AUDIO_MEDIA_PLAYER_HPP -#define __AUDIO_MEDIA_PLAYER_HPP - -#include - -#include -#include -#include -#include -#include -#include - -#include "database/Types.hpp" - -#include "av/AvTranscoder.hpp" -#include "resource/AvConvTranscodeStreamResource.hpp" -#include "resource/CoverResource.hpp" - -namespace UserInterface { -namespace Desktop { - -class AudioMediaPlayer : public Wt::WContainerWidget -{ - public: - static Wt::WMediaPlayer::Encoding getBestEncoding(); - - AudioMediaPlayer(Wt::WContainerWidget *parent = 0); - - // Load Media to be played - bool load(Database::Track::id_type trackId); - - // Accessors - Wt::WMediaPlayer::Encoding getEncoding() const { return _encoding; } - - // Signal slots - Wt::Signal& playbackEnded() {return _playbackEnded;} - Wt::Signal& playNext() {return _playNext;} - Wt::Signal& playPrevious() {return _playPrevious;} - Wt::Signal& shuffle() {return _shuffle;} - Wt::Signal& loop() {return _loop;} - - - private: - - void handlePlayOffset(int offsetSecs); - void handleTrackEnded(void); - - void handleValueChanged(double); - void handleTimeUpdated(void); - void handleSliderMoved(int value); - - void handleVolumeSliderMoved(int value); - - void loadPlayer(boost::filesystem::path filePath, Av::TranscodeParameters& parameters); - - // Signals - Wt::Signal _playbackEnded; - Wt::Signal _playNext; - Wt::Signal _playPrevious; - Wt::Signal _shuffle; - Wt::Signal _loop; - - // Core - Wt::WMediaPlayer* _mediaPlayer; - AvConvTranscodeStreamResource* _mediaResource; - Wt::WMediaPlayer::Encoding _encoding; - - // Media info - Wt::WImage* _mediaCover; - Wt::WText* _mediaTitle; - Wt::WText* _mediaArtistRelease; - - // Controls - Wt::WPushButton* _playBtn; - Wt::WPushButton* _pauseBtn; - Wt::WSlider* _timeSlider; - Wt::WSlider* _volumeSlider; - Wt::WText* _curTime; - Wt::WText* _duration; - - // Transcode - boost::filesystem::path _currentFile; - Av::TranscodeParameters _currentParameters; - -}; - -} // namespace Desktop -} // namespace UserInterface - -#endif - diff --git a/src/ui/audio/desktop/DesktopAudio.cpp b/src/ui/audio/desktop/DesktopAudio.cpp index 7e898a78..efcd13c3 100644 --- a/src/ui/audio/desktop/DesktopAudio.cpp +++ b/src/ui/audio/desktop/DesktopAudio.cpp @@ -120,73 +120,72 @@ _playQueue(nullptr) _playQueue = new PlayQueue(); // Playlist/PlayQueue + Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget(); + playQueueContainer->setStyleClass("playqueue"); + Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); + playQueueContainer->setLayout(playQueueLayout); + + playQueueLayout->addWidget( _playQueue, 1); + + Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout(); + + Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist"); + playlistBtn->setStyleClass("btn-sm btn-primary"); + playlistControls->addWidget(playlistBtn); + + // Playlist menu + { + Wt::WPopupMenu *popupMain = new Wt::WPopupMenu(); + + _popupMenuSave = new Wt::WPopupMenu(); + popupMain->addMenu("Save", _popupMenuSave); + + _popupMenuLoad = new Wt::WPopupMenu(); + popupMain->addMenu("Load", _popupMenuLoad); + + _popupMenuDelete = new Wt::WPopupMenu(); + popupMain->addMenu("Delete", _popupMenuDelete); + + playlistBtn->setMenu(popupMain); + } + + Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); + upBtn->setStyleClass("btn-sm"); + playlistControls->addWidget(upBtn); + Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); + downBtn->setStyleClass("btn-sm"); + playlistControls->addWidget(downBtn); + Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); + delBtn->setStyleClass("btn-sm btn-warning"); + playlistControls->addWidget(delBtn); + Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR"); + clearBtn->setStyleClass("btn-sm btn-danger"); + playlistControls->addWidget(clearBtn); + + delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected); + upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp); + downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown); + clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll); + + playQueueLayout->addLayout(playlistControls); + + mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1); + + // Load the last known queue + playlistLoadToPlayqueue(CurrentQueuePlaylistName); + + // Select the last known playing track { Wt::Dbo::Transaction transaction(DboSession()); - - Wt::WContainerWidget* playQueueContainer = new Wt::WContainerWidget(); - playQueueContainer->setStyleClass("playqueue"); - Wt::WVBoxLayout* playQueueLayout = new Wt::WVBoxLayout(); - playQueueContainer->setLayout(playQueueLayout); - - _mediaPlayer = new AudioMediaPlayer(); - playQueueLayout->addWidget(_mediaPlayer); - - playQueueLayout->addWidget( _playQueue, 1); - - Wt::WHBoxLayout* playlistControls = new Wt::WHBoxLayout(); - - Wt::WPushButton *playlistBtn = new Wt::WPushButton("Playlist"); - playlistBtn->setStyleClass("btn-sm btn-primary"); - playlistControls->addWidget(playlistBtn); - - // Playlist menu - { - Wt::WPopupMenu *popupMain = new Wt::WPopupMenu(); - - _popupMenuSave = new Wt::WPopupMenu(); - popupMain->addMenu("Save", _popupMenuSave); - - _popupMenuLoad = new Wt::WPopupMenu(); - popupMain->addMenu("Load", _popupMenuLoad); - - _popupMenuDelete = new Wt::WPopupMenu(); - popupMain->addMenu("Delete", _popupMenuDelete); - - playlistBtn->setMenu(popupMain); - } - - Wt::WPushButton *upBtn = new Wt::WPushButton("UP"); - upBtn->setStyleClass("btn-sm"); - playlistControls->addWidget(upBtn); - Wt::WPushButton *downBtn = new Wt::WPushButton("DO"); - downBtn->setStyleClass("btn-sm"); - playlistControls->addWidget(downBtn); - Wt::WPushButton *delBtn = new Wt::WPushButton("DEL"); - delBtn->setStyleClass("btn-sm btn-warning"); - playlistControls->addWidget(delBtn); - Wt::WPushButton *clearBtn = new Wt::WPushButton("CLR"); - clearBtn->setStyleClass("btn-sm btn-danger"); - playlistControls->addWidget(clearBtn); - - delBtn->clicked().connect(_playQueue, &PlayQueue::delSelected); - upBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedUp); - downBtn->clicked().connect(_playQueue, &PlayQueue::moveSelectedDown); - clearBtn->clicked().connect(_playQueue, &PlayQueue::delAll); - - - playQueueLayout->addLayout(playlistControls); - - mainLayout->addWidget(playQueueContainer, 0, 0, 2, 1); - - // Load the last known queue - playlistLoadToPlayqueue(CurrentQueuePlaylistName); - // Select the last known playing track _playQueue->select(CurrentUser()->getCurPlayingTrackPos()); } + _mediaPlayer = new AudioPlayer(); + mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4); + mainLayout->setRowStretch(1, 1); mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel)); - mainLayout->setColumnResizable(0, true); + mainLayout->setColumnResizable(0, true, Wt::WLength(400, Wt::WLength::Pixel)); // Double click on track // Set the selected tracks to the play queue @@ -510,7 +509,7 @@ Audio::playTrack(Track::id_type trackId, int pos) CurrentUser().modify()->setCurPlayingTrackPos(pos); } - if (!_mediaPlayer->load(trackId)) + if (!_mediaPlayer->loadTrack(trackId)) _playQueue->playNext(); } diff --git a/src/ui/audio/desktop/DesktopAudio.hpp b/src/ui/audio/desktop/DesktopAudio.hpp index f5ee34ec..ac7bda65 100644 --- a/src/ui/audio/desktop/DesktopAudio.hpp +++ b/src/ui/audio/desktop/DesktopAudio.hpp @@ -17,14 +17,13 @@ * along with LMS. If not, see . */ -#ifndef UI_AUDIO_DESKTOP_HPP -#define UI_AUDIO_DESKTOP_HPP +#pragma once #include #include -#include "AudioMediaPlayer.hpp" +#include "audio/AudioPlayer.hpp" #include "TrackView.hpp" #include "PlayQueue.hpp" @@ -66,7 +65,7 @@ class Audio : public UserInterface::Audio void handlePlaylistSelected(Wt::WString name); - AudioMediaPlayer* _mediaPlayer; + AudioPlayer* _mediaPlayer; TrackView* _trackView; PlayQueue* _playQueue; @@ -80,5 +79,3 @@ class Audio : public UserInterface::Audio } // namespace Desktop } // namespace UserInterface -#endif - diff --git a/src/ui/audio/desktop/PlayQueue.cpp b/src/ui/audio/desktop/PlayQueue.cpp index fb11cedd..294131a1 100644 --- a/src/ui/audio/desktop/PlayQueue.cpp +++ b/src/ui/audio/desktop/PlayQueue.cpp @@ -278,10 +278,8 @@ _trackSelector(new TrackSelector()) _model = new Wt::WStandardItemModel(0, 3, this); // 0 Column is hidden (track id) - _model->setHeaderData(COLUMN_ID_TRACK_ID, Wt::WString("#")); - _model->setHeaderData(COLUMN_ID_COVER, Wt::WString("Cover")); - _model->setHeaderData(COLUMN_ID_NAME, Wt::WString("Track")); + this->setHeaderHeight(0); this->setModel(_model); this->setSelectionMode(Wt::ExtendedSelection); this->setSortingEnabled(false); diff --git a/src/ui/resource/TranscodeResource.cpp b/src/ui/resource/TranscodeResource.cpp index b0a31482..882e1171 100644 --- a/src/ui/resource/TranscodeResource.cpp +++ b/src/ui/resource/TranscodeResource.cpp @@ -130,14 +130,17 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, transcoder = std::make_shared(track->getPath(), parameters); } - LMS_LOG(UI, DEBUG) << "Mime type set to '" << Av::encoding_to_mimetype(Av::Encoding::MP3); - response.setMimeType( Av::encoding_to_mimetype(Av::Encoding::MP3) ); + std::string mimeType = Av::encoding_to_mimetype(transcoder->getParameters().getEncoding()); + + LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'"; + response.setMimeType(mimeType); if (!transcoder->start()) { LMS_LOG(UI, ERROR) << "Cannot start transcoder"; return; } + LMS_LOG(UI, DEBUG) << "Transcoder started"; } @@ -146,21 +149,20 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request, std::vector data; data.reserve(_bufferSize); - LMS_LOG(UI, DEBUG) << "Reading data from transcoder"; transcoder->process(data, _bufferSize); response.out().write(reinterpret_cast(&data[0]), data.size()); - LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete(); if (!response.out()) + { LMS_LOG(UI, ERROR) << "Write failed!"; + } } if (!transcoder->isComplete() && response.out()) { continuation = response.createContinuation(); continuation->setData(transcoder); - LMS_LOG(UI, DEBUG) << "Continuation set to " << continuation; } else LMS_LOG(UI, DEBUG) << "No more data!"; diff --git a/src/ui/settings/SettingsAudioFormView.cpp b/src/ui/settings/SettingsAudioFormView.cpp index 1af1fc18..93475c48 100644 --- a/src/ui/settings/SettingsAudioFormView.cpp +++ b/src/ui/settings/SettingsAudioFormView.cpp @@ -64,7 +64,7 @@ class AudioFormModel : public Wt::WFormModel initializeModels(); addField(BitrateField); - addField(EncodingField, "Session must be reloaded to apply this setting"); + addField(EncodingField); setValidator(BitrateField, new Wt::WValidator(true)); // mandatory setValidator(EncodingField, new Wt::WValidator(true)); // mandatory