Merge branch 'audio-player' into develop
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
[Book]
|
[Book]
|
||||||
- Make the feature
|
- Make the feature?
|
||||||
|
|
||||||
[Cover]
|
[Cover]
|
||||||
- Handle preferred cover file names
|
- Handle preferred cover file names ("front.xxx", "cover.xxx", ...)
|
||||||
- Implement a cache and a grabber from some web service (mandatory for artists)
|
- Implement a cache and a grabber from some web service (mandatory for artists?)
|
||||||
|
|
||||||
[Database]
|
[Database]
|
||||||
- Optim, use SQL query to get the "genre" orphans
|
- 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
|
- 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
|
- 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?)
|
- 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]
|
[Metadata]
|
||||||
- OGG metadata -> properly handle metadata nested in the audio stream
|
- OGG metadata -> properly handle metadata nested in the audio stream
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
- Handle internationalization
|
- Handle internationalization
|
||||||
- Handle internal path
|
- Handle internal path
|
||||||
[Settings]
|
[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?
|
- "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
|
- add a scroll area in order for settings to work on mobile devices
|
||||||
[user/transcoding]
|
[user/transcoding]
|
||||||
@@ -51,20 +50,19 @@
|
|||||||
- Add a download button to get the current playlist in a streamed zip file
|
- 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
|
- Add a upload button to upload media files in a dedicated directory
|
||||||
- Perform a search for each key pressed (using a timer, in JS)
|
- 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]
|
[desktop]
|
||||||
- Better style...
|
- 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
|
- TrackView: Reselect the current selected item when displaying the updated search results
|
||||||
- Add keyboard shortcuts
|
- Add keyboard shortcuts
|
||||||
[mobile]
|
[mobile]
|
||||||
|
- Reorganize the views to ease internal paths
|
||||||
- Covers not always the same height
|
- Covers not always the same height
|
||||||
- 'duration' and 'play' tags are broken if title is too long
|
- 'duration' and 'play' tags are broken if title is too long
|
||||||
- Remove the extra search widget
|
|
||||||
- Cover loading breaks the vertical alignment
|
- Cover loading breaks the vertical alignment
|
||||||
- Implement a play queue
|
- Implement a play queue
|
||||||
- Implement a decent player
|
- Implement a decent player
|
||||||
- Make the release clickable in the Track result view
|
- Make the release/artist clickable when displayed to the user
|
||||||
- Reorganize the views to ease internal paths?
|
|
||||||
|
|
||||||
[Video]
|
[Video]
|
||||||
- implement a decent mediaplayer
|
- implement a decent mediaplayer
|
||||||
|
|||||||
+17
-1
@@ -529,7 +529,23 @@
|
|||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="wa-audio-player">
|
<message id="wa-audio-player">
|
||||||
${shuffle} ${repeat} ${playlist} ${prev} ${play-pause} ${next} ${cover} ${artist} ${track} ${release} ${curtime} ${seekbar} ${duration} ${volume}
|
${audio}
|
||||||
|
${prev}${play-pause}${next}
|
||||||
|
<div class="mediaplayer-volume-container visible-lg">
|
||||||
|
${volume}
|
||||||
|
</div>
|
||||||
|
<div class="mediaplayer-cover hidden-xs">
|
||||||
|
${cover}
|
||||||
|
</div>
|
||||||
|
<div class="mediaplayer-info-container">
|
||||||
|
<div class="mediaplayer-track-info">
|
||||||
|
<span class="mediaplayer-track">${track}</span> <span class="mediaplayer-artist">${artist}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mediaplayer-progress-container">
|
||||||
|
${curtime}${seekbar}${duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${shuffle}${repeat}${playlist}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
</messages>
|
</messages>
|
||||||
|
|||||||
+87
-24
@@ -1,5 +1,6 @@
|
|||||||
.main-nav {
|
.main-nav {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Wt-hrh2 {
|
.Wt-hrh2 {
|
||||||
@@ -10,14 +11,10 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.contents {
|
|
||||||
padding: 0px 12px 6px;
|
|
||||||
margin-bottom: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playqueue {
|
.playqueue {
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.playqueue-playing {
|
.playqueue-playing {
|
||||||
@@ -40,32 +37,98 @@ div.contents {
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaplayer {
|
.mediaplayer-track {
|
||||||
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 {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mediaplayer-current-artist {
|
.mediaplayer-artist {
|
||||||
font-style: italic;
|
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 {
|
.mobile-search-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ lms_SOURCES = \
|
|||||||
$(srcdir)/ui/auth/LmsAuth.cpp \
|
$(srcdir)/ui/auth/LmsAuth.cpp \
|
||||||
$(srcdir)/ui/audio/AudioPlayer.cpp \
|
$(srcdir)/ui/audio/AudioPlayer.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
|
$(srcdir)/ui/audio/desktop/DesktopAudio.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/AudioMediaPlayer.cpp \
|
|
||||||
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
|
$(srcdir)/ui/audio/desktop/FilterChain.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
|
$(srcdir)/ui/audio/desktop/KeywordSearchFilter.cpp \
|
||||||
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
|
$(srcdir)/ui/audio/desktop/PlayQueue.cpp \
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class Transcoder
|
|||||||
void process(std::vector<unsigned char>& output, std::size_t maxSize);
|
void process(std::vector<unsigned char>& output, std::size_t maxSize);
|
||||||
bool isComplete(void) { return _isComplete; }
|
bool isComplete(void) { return _isComplete; }
|
||||||
|
|
||||||
|
const TranscodeParameters& getParameters() const { return _parameters; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Transcoder();
|
Transcoder();
|
||||||
|
|
||||||
|
|||||||
+180
-87
@@ -33,21 +33,49 @@
|
|||||||
|
|
||||||
namespace UserInterface {
|
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)
|
AudioPlayer::loadTrack(Database::Track::id_type trackId)
|
||||||
{
|
{
|
||||||
|
// TODO reduce this scope
|
||||||
Wt::Dbo::Transaction transaction(DboSession());
|
Wt::Dbo::Transaction transaction(DboSession());
|
||||||
|
|
||||||
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
Database::Track::pointer track = Database::Track::getById(DboSession(), trackId);
|
||||||
if (!track)
|
if (!track)
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, INFO) << "No track found for id " << trackId;
|
LMS_LOG(UI, INFO) << "No track found for id " << trackId;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_trackName->setText(Wt::WString::fromUTF8(track->getName()));
|
Database::User::pointer user = CurrentUser();
|
||||||
_artistName->setText( Wt::WString::fromUTF8(track->getArtist()->getName()));
|
if (!user)
|
||||||
_releaseName->setText( Wt::WString::fromUTF8(track->getRelease()->getName()));
|
{
|
||||||
|
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));
|
_cover->setImageLink(SessionCoverResource()->getTrackUrl(trackId, 64));
|
||||||
_trackDuration->setText( boost::posix_time::to_simple_string( track->getDuration() ));
|
_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())
|
if (!mediaFile.open() || !mediaFile.scan())
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, ERROR) << "Cannot open file '" << track->getPath();
|
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;\
|
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?
|
//TODO, try to load everything in JS in order to prevent the WriteError bug?
|
||||||
_audio->pause();
|
_audio->pause();
|
||||||
_audio->clearSources();
|
_audio->clearSources();
|
||||||
//TODOencoding
|
//TODO, encoding
|
||||||
_audio->addSource(SessionTranscodeResource()->getUrl(trackId, Av::Encoding::MP3, 0, streams));
|
_audio->addSource(SessionTranscodeResource()->getUrl(trackId, encoding, 0, streams));
|
||||||
|
_audio->setPreloadMode(Wt::WAudio::PreloadAuto);
|
||||||
_audio->play();
|
_audio->play();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
|
AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
|
||||||
: Wt::WContainerWidget(parent)
|
: Wt::WTemplate(parent)
|
||||||
{
|
{
|
||||||
Wt::WTemplate *t = new Wt::WTemplate(this);
|
setTemplateText(Wt::WString::tr("wa-audio-player"));
|
||||||
t->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->setOptions(Wt::WAudio::Autoplay);
|
||||||
_audio->setPreloadMode(Wt::WAudio::PreloadAuto);
|
_audio->setPreloadMode(Wt::WAudio::PreloadAuto);
|
||||||
|
bindWidget("audio", _audio);
|
||||||
|
|
||||||
|
_audio->ended().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playbackEnded.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
_cover = new Wt::WImage();
|
_cover = new Wt::WImage();
|
||||||
t->bindWidget("cover", _cover);
|
bindWidget("cover", _cover);
|
||||||
_cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64));
|
_cover->setImageLink(SessionCoverResource()->getUnknownTrackUrl(64));
|
||||||
|
|
||||||
InputRange *seekbar = new InputRange();
|
InputRange *seekbar = new InputRange();
|
||||||
t->bindWidget("seekbar", seekbar);
|
seekbar->addStyleClass("mediaplayer-seekbar");
|
||||||
|
bindWidget("seekbar", seekbar);
|
||||||
|
|
||||||
_trackName = new Wt::WText();
|
bindString("track", "Track");
|
||||||
t->bindWidget("track", _trackName);
|
bindString("artist", "Artist");
|
||||||
|
|
||||||
_artistName = new Wt::WText();
|
|
||||||
t->bindWidget("artist", _artistName);
|
|
||||||
|
|
||||||
_releaseName = new Wt::WText();
|
|
||||||
t->bindWidget("release", _releaseName);
|
|
||||||
|
|
||||||
InputRange *volumeSlider = new InputRange();
|
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("<i class=\"fa fa-list fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *playlistBtn = new Wt::WText("<i class=\"fa fa-list fa-2x\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("playlist", playlistBtn);
|
playlistBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("playlist", playlistBtn);
|
||||||
|
|
||||||
Wt::WPushButton *repeatBtn = new Wt::WPushButton("<i class=\"fa fa-repeat fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *repeatBtn = new Wt::WText("<i class=\"fa fa-repeat fa-2x\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("repeat", repeatBtn);
|
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("<i class=\"fa fa-random fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *shuffleBtn = new Wt::WText("<i class=\"fa fa-random fa-2x\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("shuffle", shuffleBtn);
|
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("<i class=\"fa fa-step-backward fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *prevBtn = new Wt::WText("<i class=\"fa fa-step-backward fa-2x\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("prev", prevBtn);
|
prevBtn->addStyleClass("mediaplayer-btn hidden-xs");
|
||||||
|
bindWidget("prev", prevBtn);
|
||||||
|
prevBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playPrevious.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
Wt::WPushButton *nextBtn = new Wt::WPushButton("<i class=\"fa fa-step-forward fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *nextBtn = new Wt::WText("<i class=\"fa fa-step-forward fa-2x\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("next", nextBtn);
|
nextBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("next", nextBtn);
|
||||||
|
nextBtn->clicked().connect(std::bind([=] ()
|
||||||
|
{
|
||||||
|
_playNext.emit();
|
||||||
|
}));
|
||||||
|
|
||||||
Wt::WPushButton *playPauseBtn = new Wt::WPushButton("<i class=\"fa fa-play fa-lg\"></i>", Wt::XHTMLText);
|
Wt::WText *playPauseBtn = new Wt::WText("<i class=\"fa fa-play fa-3x fa-fw\"></i>", Wt::XHTMLText);
|
||||||
t->bindWidget("play-pause", playPauseBtn);
|
playPauseBtn->addStyleClass("mediaplayer-btn");
|
||||||
|
bindWidget("play-pause", playPauseBtn);
|
||||||
|
|
||||||
Wt::WText *trackCurrentTime = new Wt::WText("00:00");
|
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");
|
_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(
|
this->doJavaScript(
|
||||||
"\
|
"\
|
||||||
document.lms = {};\
|
document.lms = {};\
|
||||||
document.lms.audio = {};\
|
document.lms.audio = {};\
|
||||||
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
document.lms.audio.audio = " + _audio->jsRef() + ";\
|
||||||
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
document.lms.audio.seekbar = " + seekbar->jsRef() +";\
|
||||||
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
document.lms.audio.volumeSlider = " + volumeSlider->jsRef() + ";\
|
||||||
document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\
|
document.lms.audio.curTimeText = " + trackCurrentTime->jsRef() + ";\
|
||||||
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
document.lms.audio.playPause = " + playPauseBtn->jsRef() + ";\
|
||||||
\
|
\
|
||||||
document.lms.audio.offset = 0;\
|
document.lms.audio.offset = 0;\
|
||||||
document.lms.audio.curTime = 0;\
|
document.lms.audio.curTime = 0;\
|
||||||
document.lms.audio.state = \"init\";\
|
document.lms.audio.state = \"init\";\
|
||||||
document.lms.audio.volume = 1;\
|
document.lms.audio.volume = 1;\
|
||||||
\
|
\
|
||||||
document.lms.audio.seekbar.value = 0;\
|
document.lms.audio.seekbar.value = 0;\
|
||||||
document.lms.audio.seekbar.disabled = true;\
|
document.lms.audio.seekbar.disabled = true;\
|
||||||
\
|
\
|
||||||
document.lms.audio.volumeSlider.min = 0;\
|
document.lms.audio.volumeSlider.min = 0;\
|
||||||
document.lms.audio.volumeSlider.max = 100;\
|
document.lms.audio.volumeSlider.max = 100;\
|
||||||
document.lms.audio.volumeSlider.value = 100;\
|
document.lms.audio.volumeSlider.value = 100;\
|
||||||
\
|
\
|
||||||
function updateUI() {\
|
function updateUIPlaying() { \
|
||||||
document.lms.audio.curTimeText.innerHTML = document.lms.audio.curTime;\
|
var icon = document.lms.audio.playPause.getElementsByTagName(\"i\")[0]; \
|
||||||
document.lms.audio.seekbar.value = document.lms.audio.curTime;\
|
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;\
|
var mouseDown = 0;\
|
||||||
function seekMouseDown(e) {\
|
function seekMouseDown(e) {\
|
||||||
++mouseDown;\
|
++mouseDown;\
|
||||||
}\
|
}\
|
||||||
function seekMouseUp(e) {\
|
function seekMouseUp(e) {\
|
||||||
--mouseDown;\
|
--mouseDown;\
|
||||||
}\
|
}\
|
||||||
\
|
\
|
||||||
function seeking(e) {\
|
function seeking(e) {\
|
||||||
if (document.lms.audio.state == \"init\")\
|
if (document.lms.audio.state == \"init\")\
|
||||||
return;\
|
return;\
|
||||||
\
|
\
|
||||||
document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\
|
document.lms.audio.curTimeText.innerHTML = document.lms.audio.seekbar.value;\
|
||||||
}\
|
}\
|
||||||
\
|
\
|
||||||
function seek(e) {\
|
function seek(e) {\
|
||||||
if (document.lms.audio.state == \"init\")\
|
if (document.lms.audio.state == \"init\")\
|
||||||
return;\
|
return;\
|
||||||
\
|
\
|
||||||
document.lms.audio.audio.pause(); \
|
document.lms.audio.audio.pause(); \
|
||||||
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
document.lms.audio.offset = parseInt(document.lms.audio.seekbar.value);\
|
||||||
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
document.lms.audio.curTime = document.lms.audio.seekbar.value;\
|
||||||
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
var audioSource = document.lms.audio.audio.getElementsByTagName(\"source\")[0];\
|
||||||
var src = audioSource.src;\
|
var src = audioSource.src;\
|
||||||
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
src = src.slice(0, src.lastIndexOf(\"=\") + 1);\
|
||||||
audioSource.src = src + document.lms.audio.seekbar.value;\
|
audioSource.src = src + document.lms.audio.seekbar.value;\
|
||||||
document.lms.audio.audio.load(); \
|
document.lms.audio.audio.load(); \
|
||||||
document.lms.audio.audio.play(); \
|
document.lms.audio.audio.play(); \
|
||||||
document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\
|
document.lms.audio.curTimeText.innerHTML = ~~document.lms.audio.curTime + \" \";\
|
||||||
}\
|
}\
|
||||||
\
|
\
|
||||||
function volumeChanged() {\
|
function volumeChanged() {\
|
||||||
@@ -205,26 +294,30 @@ AudioPlayer::AudioPlayer(Wt::WContainerWidget *parent)
|
|||||||
updateUI();\
|
updateUI();\
|
||||||
} \
|
} \
|
||||||
\
|
\
|
||||||
function playPause() {\
|
function playPause() { \
|
||||||
if (document.lms.audio.state == \"init\") \
|
if (document.lms.audio.state == \"init\") \
|
||||||
return;\
|
return;\
|
||||||
\
|
\
|
||||||
if (document.lms.audio.audio.paused)\
|
if (document.lms.audio.audio.paused) \
|
||||||
document.lms.audio.audio.play();\
|
document.lms.audio.audio.play(); \
|
||||||
else\
|
else \
|
||||||
document.lms.audio.audio.pause();\
|
document.lms.audio.audio.pause();\
|
||||||
\
|
\
|
||||||
}\
|
}\
|
||||||
\
|
\
|
||||||
document.lms.audio.audio.addEventListener('timeupdate', updateCurTime); \
|
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('change', seek);\
|
||||||
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
document.lms.audio.seekbar.addEventListener('input', seeking);\
|
||||||
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
document.lms.audio.seekbar.addEventListener('mousedown', seekMouseDown);\
|
||||||
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
document.lms.audio.seekbar.addEventListener('mouseup', seekMouseUp);\
|
||||||
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
document.lms.audio.volumeSlider.addEventListener('input', volumeChanged);\
|
||||||
document.lms.audio.playPause.addEventListener('click', playPause);\
|
document.lms.audio.playPause.addEventListener('click', playPause);\
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,28 +19,43 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WTemplate>
|
||||||
|
#include <Wt/WSignal>
|
||||||
|
|
||||||
#include "common/InputRange.hpp"
|
#include "common/InputRange.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
#include "av/AvTranscoder.hpp"
|
||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
class AudioPlayer : public Wt::WContainerWidget
|
class AudioPlayer : public Wt::WTemplate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
||||||
AudioPlayer(Wt::WContainerWidget *parent = 0);
|
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<void>& playbackEnded() {return _playbackEnded;}
|
||||||
|
Wt::Signal<void>& playNext() {return _playNext;}
|
||||||
|
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
||||||
|
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
||||||
|
Wt::Signal<bool>& loop() {return _loop;}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
// Signals
|
||||||
|
Wt::Signal<void> _playbackEnded;
|
||||||
|
Wt::Signal<void> _playNext;
|
||||||
|
Wt::Signal<void> _playPrevious;
|
||||||
|
Wt::Signal<bool> _shuffle;
|
||||||
|
Wt::Signal<bool> _loop;
|
||||||
|
|
||||||
Wt::WAudio* _audio;
|
Wt::WAudio* _audio;
|
||||||
Wt::WText* _trackDuration;
|
Wt::WText* _trackDuration;
|
||||||
Wt::WText* _trackName;
|
Wt::WText* _trackName;
|
||||||
Wt::WText* _artistName;
|
|
||||||
Wt::WText* _releaseName;
|
|
||||||
Wt::WImage* _cover;
|
Wt::WImage* _cover;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WProgressBar>
|
|
||||||
#include <Wt/WCheckBox>
|
|
||||||
#include <Wt/WHBoxLayout>
|
|
||||||
#include <Wt/WVBoxLayout>
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __AUDIO_MEDIA_PLAYER_HPP
|
|
||||||
#define __AUDIO_MEDIA_PLAYER_HPP
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include <Wt/WSlider>
|
|
||||||
#include <Wt/WPushButton>
|
|
||||||
#include <Wt/WContainerWidget>
|
|
||||||
#include <Wt/WMediaPlayer>
|
|
||||||
#include <Wt/WText>
|
|
||||||
#include <Wt/WImage>
|
|
||||||
|
|
||||||
#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<void>& playbackEnded() {return _playbackEnded;}
|
|
||||||
Wt::Signal<void>& playNext() {return _playNext;}
|
|
||||||
Wt::Signal<void>& playPrevious() {return _playPrevious;}
|
|
||||||
Wt::Signal<bool>& shuffle() {return _shuffle;}
|
|
||||||
Wt::Signal<bool>& 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<void> _playbackEnded;
|
|
||||||
Wt::Signal<void> _playNext;
|
|
||||||
Wt::Signal<void> _playPrevious;
|
|
||||||
Wt::Signal<bool> _shuffle;
|
|
||||||
Wt::Signal<bool> _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
|
|
||||||
|
|
||||||
@@ -120,73 +120,72 @@ _playQueue(nullptr)
|
|||||||
_playQueue = new PlayQueue();
|
_playQueue = new PlayQueue();
|
||||||
|
|
||||||
// Playlist/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::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());
|
_playQueue->select(CurrentUser()->getCurPlayingTrackPos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mediaPlayer = new AudioPlayer();
|
||||||
|
mainLayout->addWidget(_mediaPlayer, 2, 0, 1, 4);
|
||||||
|
|
||||||
mainLayout->setRowStretch(1, 1);
|
mainLayout->setRowStretch(1, 1);
|
||||||
mainLayout->setRowResizable(0, true, Wt::WLength(250, Wt::WLength::Pixel));
|
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
|
// Double click on track
|
||||||
// Set the selected tracks to the play queue
|
// Set the selected tracks to the play queue
|
||||||
@@ -510,7 +509,7 @@ Audio::playTrack(Track::id_type trackId, int pos)
|
|||||||
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
CurrentUser().modify()->setCurPlayingTrackPos(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_mediaPlayer->load(trackId))
|
if (!_mediaPlayer->loadTrack(trackId))
|
||||||
_playQueue->playNext();
|
_playQueue->playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,13 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef UI_AUDIO_DESKTOP_HPP
|
#pragma once
|
||||||
#define UI_AUDIO_DESKTOP_HPP
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <Wt/WPopupMenu>
|
#include <Wt/WPopupMenu>
|
||||||
|
|
||||||
#include "AudioMediaPlayer.hpp"
|
#include "audio/AudioPlayer.hpp"
|
||||||
#include "TrackView.hpp"
|
#include "TrackView.hpp"
|
||||||
#include "PlayQueue.hpp"
|
#include "PlayQueue.hpp"
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ class Audio : public UserInterface::Audio
|
|||||||
|
|
||||||
void handlePlaylistSelected(Wt::WString name);
|
void handlePlaylistSelected(Wt::WString name);
|
||||||
|
|
||||||
AudioMediaPlayer* _mediaPlayer;
|
AudioPlayer* _mediaPlayer;
|
||||||
TrackView* _trackView;
|
TrackView* _trackView;
|
||||||
PlayQueue* _playQueue;
|
PlayQueue* _playQueue;
|
||||||
|
|
||||||
@@ -80,5 +79,3 @@ class Audio : public UserInterface::Audio
|
|||||||
} // namespace Desktop
|
} // namespace Desktop
|
||||||
} // namespace UserInterface
|
} // namespace UserInterface
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|||||||
@@ -278,10 +278,8 @@ _trackSelector(new TrackSelector())
|
|||||||
_model = new Wt::WStandardItemModel(0, 3, this);
|
_model = new Wt::WStandardItemModel(0, 3, this);
|
||||||
|
|
||||||
// 0 Column is hidden (track id)
|
// 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->setModel(_model);
|
||||||
this->setSelectionMode(Wt::ExtendedSelection);
|
this->setSelectionMode(Wt::ExtendedSelection);
|
||||||
this->setSortingEnabled(false);
|
this->setSortingEnabled(false);
|
||||||
|
|||||||
@@ -130,14 +130,17 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
|
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Mime type set to '" << Av::encoding_to_mimetype(Av::Encoding::MP3);
|
std::string mimeType = Av::encoding_to_mimetype(transcoder->getParameters().getEncoding());
|
||||||
response.setMimeType( Av::encoding_to_mimetype(Av::Encoding::MP3) );
|
|
||||||
|
LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'";
|
||||||
|
response.setMimeType(mimeType);
|
||||||
|
|
||||||
if (!transcoder->start())
|
if (!transcoder->start())
|
||||||
{
|
{
|
||||||
LMS_LOG(UI, ERROR) << "Cannot start transcoder";
|
LMS_LOG(UI, ERROR) << "Cannot start transcoder";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Transcoder started";
|
LMS_LOG(UI, DEBUG) << "Transcoder started";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,21 +149,20 @@ TranscodeResource::handleRequest(const Wt::Http::Request& request,
|
|||||||
std::vector<unsigned char> data;
|
std::vector<unsigned char> data;
|
||||||
data.reserve(_bufferSize);
|
data.reserve(_bufferSize);
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Reading data from transcoder";
|
|
||||||
transcoder->process(data, _bufferSize);
|
transcoder->process(data, _bufferSize);
|
||||||
|
|
||||||
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||||
|
|
||||||
LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete();
|
LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << std::boolalpha << transcoder->isComplete();
|
||||||
|
|
||||||
if (!response.out())
|
if (!response.out())
|
||||||
|
{
|
||||||
LMS_LOG(UI, ERROR) << "Write failed!";
|
LMS_LOG(UI, ERROR) << "Write failed!";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transcoder->isComplete() && response.out()) {
|
if (!transcoder->isComplete() && response.out()) {
|
||||||
continuation = response.createContinuation();
|
continuation = response.createContinuation();
|
||||||
continuation->setData(transcoder);
|
continuation->setData(transcoder);
|
||||||
LMS_LOG(UI, DEBUG) << "Continuation set to " << continuation;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LMS_LOG(UI, DEBUG) << "No more data!";
|
LMS_LOG(UI, DEBUG) << "No more data!";
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class AudioFormModel : public Wt::WFormModel
|
|||||||
initializeModels();
|
initializeModels();
|
||||||
|
|
||||||
addField(BitrateField);
|
addField(BitrateField);
|
||||||
addField(EncodingField, "Session must be reloaded to apply this setting");
|
addField(EncodingField);
|
||||||
|
|
||||||
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
setValidator(BitrateField, new Wt::WValidator(true)); // mandatory
|
||||||
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
setValidator(EncodingField, new Wt::WValidator(true)); // mandatory
|
||||||
|
|||||||
Reference in New Issue
Block a user