Commit 36d07598 authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Split thumbnail generation & add new core thumbnailer support

parent 919f08e7
......@@ -215,6 +215,16 @@ endif
if HAVE_LIBVLC
if HAVE_LIBVLC4
libmedialibrary_la_SOURCES += \
src/metadata_services/vlc/CoreThumbnailer.cpp \
src/metadata_services/vlc/CoreThumbnailer.h
else
libmedialibrary_la_SOURCES += \
src/metadata_services/vlc/VmemThumbnailer.cpp \
src/metadata_services/vlc/VmemThumbnailer.h
if HAVE_LIBJPEG
libmedialibrary_la_SOURCES += src/metadata_services/vlc/imagecompressors/JpegCompressor.cpp
noinst_HEADERS += src/metadata_services/vlc/imagecompressors/JpegCompressor.h
......@@ -222,6 +232,8 @@ libmedialibrary_la_CPPFLAGS += $(LIBJPEG_CFLAGS)
libmedialibrary_la_LIBADD += $(LIBJPEG_LIBS)
endif
endif
libmedialibrary_la_SOURCES += \
src/metadata_services/vlc/VLCMetadataService.cpp \
src/metadata_services/vlc/VLCThumbnailer.cpp \
......
......@@ -263,7 +263,9 @@ AC_ARG_WITH(libvlc, [AC_HELP_STRING([--with-libvlc], [build with libvlc integrat
AH_TEMPLATE(HAVE_LIBVLC, [Define to 1 if libvlc integration is enabled])
AS_IF([test "${with_libvlc}" != "no"], [
AC_DEFINE(HAVE_LIBVLC)
PKG_CHECK_MODULES(VLC, libvlc >= 3.0)
PKG_CHECK_MODULES(VLC, libvlc >= 4.0, [have_libvlc4="yes"], [
PKG_CHECK_MODULES(VLC, libvlc >= 3.0)
])
PKG_CHECK_MODULES(VLCPP, libvlcpp,
[AC_MSG_RESULT([Found libvlcpp.pc])],
[
......@@ -285,6 +287,7 @@ AS_IF([test "${with_libvlc}" != "no"], [
])
])
AM_CONDITIONAL(HAVE_LIBVLC, [test "${with_libvlc}" != "no"])
AM_CONDITIONAL(HAVE_LIBVLC4, [test "${have_libvlc4}" = "yes" ])
AM_CONDITIONAL([HAVE_LIBJPEG], [test "${have_libjpeg}" = "yes"])
PKG_CHECK_MODULES(SQLITE, sqlite3)
......
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2018 Hugo Beauzée-Luyssen, Videolabs, VideoLAN
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "CoreThumbnailer.h"
#include "compat/ConditionVariable.h"
#include "compat/Mutex.h"
#include "Media.h"
#include "MediaLibrary.h"
#include "utils/VLCInstance.h"
#include <vlcpp/vlc.hpp>
namespace medialibrary
{
CoreThumbnailer::CoreThumbnailer( MediaLibraryPtr ml )
: m_ml( ml )
{
}
bool CoreThumbnailer::generate( medialibrary::MediaPtr media, const std::string& mrl )
{
VLC::Media vlcMedia{ VLCInstance::get(), mrl, VLC::Media::FromType::FromLocation };
auto em = vlcMedia.eventManager();
compat::ConditionVariable cond;
compat::Mutex lock;
auto done = false;
VLC::Picture thumbnail;
em.onThumbnailGenerated([&cond, &lock, &thumbnail, &done]( const VLC::Picture* p ) {
{
std::unique_lock<compat::Mutex> l( lock );
if( p != nullptr )
thumbnail = *p;
done = true;
}
cond.notify_all();
});
{
std::unique_lock<compat::Mutex> l( lock );
auto request = vlcMedia.thumbnailRequestByPos( 0.3f, VLC::Media::ThumbnailSeekSpeed::Fast,
320, 200, VLC::Picture::Type::Jpg, 3000 );
if ( request == nullptr )
return false;
cond.wait( l, [&done]() { return done == true; } );
}
if ( thumbnail.isValid() == false )
return false;
auto path = m_ml->thumbnailPath() + "/" + std::to_string( media->id() ) + ".jpg";
if ( thumbnail.save( path ) == false )
return false;
auto m = static_cast<Media*>( media.get() );
return m->setThumbnail( path, Thumbnail::Origin::Media, true );
}
}
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015-2018 Hugo Beauzée-Luyssen, Videolabs, VideoLAN
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#pragma once
#include "VLCThumbnailer.h"
namespace medialibrary
{
class CoreThumbnailer : public VLCThumbnailer::Generator
{
public:
CoreThumbnailer( MediaLibraryPtr ml );
virtual bool generate( MediaPtr media, const std::string& mrl ) override;
private:
MediaLibraryPtr m_ml;
};
}
......@@ -30,21 +30,19 @@
#include "VLCThumbnailer.h"
#include "AlbumTrack.h"
#include "Album.h"
#include "Artist.h"
#include "Media.h"
#include "File.h"
#include "logging/Logger.h"
#include "MediaLibrary.h"
#include "utils/VLCInstance.h"
#include "utils/ModificationsNotifier.h"
#include "metadata_services/vlc/Common.hpp"
#ifdef HAVE_JPEG
#include "imagecompressors/JpegCompressor.h"
#include <vlcpp/vlc.hpp>
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(4, 0, 0, 0)
#include "CoreThumbnailer.h"
#else
#error No image compressor available
#include "VmemThumbnailer.h"
#endif
namespace medialibrary
......@@ -54,10 +52,11 @@ VLCThumbnailer::VLCThumbnailer( MediaLibraryPtr ml )
: m_ml( ml )
, m_run( false )
, m_paused( false )
, m_prevSize( 0 )
{
#ifdef HAVE_JPEG
m_compressor.reset( new JpegCompressor );
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(4, 0, 0, 0)
m_generator.reset( new CoreThumbnailer( m_ml ) );
#else
m_generator.reset( new VmemThumbnailer( m_ml ) );
#endif
}
......@@ -70,8 +69,7 @@ void VLCThumbnailer::requestThumbnail( MediaPtr media )
{
{
std::unique_lock<compat::Mutex> lock( m_mutex );
auto task = std::unique_ptr<Task>( new Task( std::move( media ) ) );
m_tasks.push( std::move( task ) );
m_tasks.push( std::move( media ) );
}
if ( m_thread.get_id() == compat::Thread::id{} )
{
......@@ -102,7 +100,7 @@ void VLCThumbnailer::run()
LOG_INFO( "Starting thumbnailer thread" );
while ( m_run == true )
{
std::unique_ptr<Task> task;
MediaPtr media;
{
std::unique_lock<compat::Mutex> lock( m_mutex );
if ( m_tasks.size() == 0 || m_paused == true )
......@@ -114,11 +112,11 @@ void VLCThumbnailer::run()
if ( m_run == false )
break;
}
task = std::move( m_tasks.front() );
media = std::move( m_tasks.front() );
m_tasks.pop();
}
bool res = generateThumbnail( *task );
m_ml->getCb()->onMediaThumbnailReady( task->media, res );
bool res = generateThumbnail( media );
m_ml->getCb()->onMediaThumbnailReady( media, res );
}
LOG_INFO( "Exiting thumbnailer thread" );
}
......@@ -138,193 +136,25 @@ void VLCThumbnailer::stop()
}
}
bool VLCThumbnailer::generateThumbnail( Task& task )
bool VLCThumbnailer::generateThumbnail( MediaPtr media )
{
assert( task.media->type() != Media::Type::Audio );
assert( media->type() != Media::Type::Audio );
auto files = task.media->files();
auto files = media->files();
if ( files.empty() == true )
{
LOG_WARN( "Can't generate thumbnail for a media without associated files (",
task.media->title() );
return false;
}
task.mrl = files[0]->mrl();
LOG_INFO( "Generating ", task.mrl, " thumbnail..." );
VLC::Media vlcMedia = VLC::Media( VLCInstance::get(), task.mrl,
VLC::Media::FromType::FromLocation );
vlcMedia.addOption( ":no-audio" );
vlcMedia.addOption( ":no-osd" );
vlcMedia.addOption( ":no-spu" );
vlcMedia.addOption( ":input-fast-seek" );
vlcMedia.addOption( ":avcodec-hw=none" );
vlcMedia.addOption( ":no-mkv-preload-local-dir" );
auto duration = vlcMedia.duration();
if ( duration > 0 )
{
std::ostringstream ss;
// Duration is in ms, start-time in seconds, and we're aiming at 1/4th of the media
ss << ":start-time=" << duration / 4000;
vlcMedia.addOption( ss.str() );
}
task.mp = VLC::MediaPlayer( vlcMedia );
setupVout( task );
auto res = MetadataCommon::startPlayback( vlcMedia, task.mp );
if ( res == false )
{
LOG_WARN( "Failed to generate ", task.mrl, " thumbnail: Can't start playback" );
media->title() );
return false;
}
auto mrl = files[0]->mrl();
if ( duration <= 0 )
{
// Seek ahead to have a significant preview
res = seekAhead( task );
if ( res == false )
{
LOG_WARN( "Failed to generate ", task.mrl, " thumbnail: Failed to seek ahead" );
return false;
}
}
res = takeThumbnail( task );
if ( res == false )
return false;
LOG_INFO( "Done generating ", task.mrl, " thumbnail" );
m_ml->getNotifier()->notifyMediaModification( task.media );
return true;
}
bool VLCThumbnailer::seekAhead( Task& task )
{
float pos = .0f;
auto event = task.mp.eventManager().onPositionChanged([&task, &pos](float p) {
std::unique_lock<compat::Mutex> lock( task.mutex );
pos = p;
task.cond.notify_all();
});
auto success = false;
{
std::unique_lock<compat::Mutex> lock( task.mutex );
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(4, 0, 0, 0)
task.mp.setPosition( .4f, true );
#else
task.mp.setPosition( .4f );
#endif
success = task.cond.wait_for( lock, std::chrono::seconds( 3 ), [&pos]() {
return pos >= .1f;
});
}
// Since we're locking a mutex for each position changed, let's unregister ASAP
event->unregister();
return success;
}
void VLCThumbnailer::setupVout( Task& task )
{
task.mp.setVideoFormatCallbacks(
// Setup
[this, &task](char* chroma, unsigned int* width, unsigned int *height,
unsigned int *pitches, unsigned int *lines) {
strcpy( chroma, m_compressor->fourCC() );
const float inputAR = (float)*width / *height;
task.width = DesiredWidth;
task.height = (float)task.width / inputAR + 1;
if ( task.height < DesiredHeight )
{
// Avoid downscaling too much for really wide pictures
task.width = inputAR * DesiredHeight;
task.height = DesiredHeight;
}
auto size = task.width * task.height * m_compressor->bpp();
// If our buffer isn't enough anymore, reallocate a new one.
if ( size > m_prevSize )
{
m_buff.reset( new uint8_t[size] );
m_prevSize = size;
}
*width = task.width;
*height = task.height;
*pitches = task.width * m_compressor->bpp();
*lines = task.height;
return 1;
},
// Cleanup
nullptr);
task.mp.setVideoCallbacks(
// Lock
[this](void** pp_buff) {
*pp_buff = m_buff.get();
return nullptr;
},
//unlock
nullptr,
//display
[&task](void*) {
bool expected = true;
if ( task.thumbnailRequired.compare_exchange_strong( expected, false ) )
{
task.cond.notify_all();
}
}
);
}
bool VLCThumbnailer::takeThumbnail( Task& task )
{
// lock, signal that we want a thumbnail, and wait.
{
std::unique_lock<compat::Mutex> lock( task.mutex );
task.thumbnailRequired = true;
bool success = task.cond.wait_for( lock, std::chrono::seconds( 15 ), [&task]() {
// Keep waiting if the vmem thread hasn't restored m_thumbnailRequired to false
return task.thumbnailRequired == false;
});
if ( success == false )
{
LOG_WARN( "Timed out while computing ", task.mrl, " snapshot" );
return false;
}
}
task.mp.stop();
return compress( task );
}
bool VLCThumbnailer::compress( Task& task )
{
auto path = m_ml->thumbnailPath();
path += "/";
path += std::to_string( task.media->id() ) + "." + m_compressor->extension();
auto hOffset = task.width > DesiredWidth ? ( task.width - DesiredWidth ) / 2 : 0;
auto vOffset = task.height > DesiredHeight ? ( task.height - DesiredHeight ) / 2 : 0;
if ( m_compressor->compress( m_buff.get(), path, task.width, task.height,
DesiredWidth, DesiredHeight, hOffset, vOffset ) == false )
LOG_INFO( "Generating ", mrl, " thumbnail..." );
if ( m_generator->generate( media, mrl ) == false )
return false;
auto media = static_cast<Media*>( task.media.get() );
media->setThumbnail( path, Thumbnail::Origin::Media, true );
m_ml->getNotifier()->notifyMediaModification( media );
return true;
}
VLCThumbnailer::Task::Task( MediaPtr m )
: media( std::move( m ) )
, width( 0 )
, height( 0 )
, thumbnailRequired( false )
{
}
}
......@@ -23,32 +23,26 @@
#pragma once
#include "compat/ConditionVariable.h"
#include "compat/Thread.h"
#include "medialibrary/Types.h"
#include "Types.h"
#include <vlcpp/vlc.hpp>
#include "imagecompressors/IImageCompressor.h"
#include "parser/ParserWorker.h"
#include <queue>
#include <atomic>
namespace medialibrary
{
class VLCThumbnailer
{
struct Task
public:
class Generator
{
Task( MediaPtr media );
compat::Mutex mutex;
compat::ConditionVariable cond;
MediaPtr media;
std::string mrl;
uint32_t width;
uint32_t height;
VLC::MediaPlayer mp;
std::atomic_bool thumbnailRequired;
public:
virtual ~Generator() = default;
virtual bool generate( MediaPtr media, const std::string& mrl ) = 0;
};
public:
explicit VLCThumbnailer( MediaLibraryPtr ml );
virtual ~VLCThumbnailer();
void requestThumbnail( MediaPtr media );
......@@ -59,29 +53,20 @@ private:
void run();
void stop();
bool generateThumbnail( Task& task );
bool seekAhead( Task& task );
void setupVout( Task& task );
bool takeThumbnail( Task& task );
bool compress( Task& task );
bool generateThumbnail( MediaPtr task );
private:
// Force a base width, let height be computed depending on A/R
static const uint32_t DesiredWidth = 320;
static const uint32_t DesiredHeight = 200; // Aim for a 16:10 thumbnail
private:
MediaLibraryPtr m_ml;
compat::Mutex m_mutex;
compat::ConditionVariable m_cond;
std::queue<std::unique_ptr<Task>> m_tasks;
std::queue<MediaPtr> m_tasks;
std::atomic_bool m_run;
std::unique_ptr<Generator> m_generator;
compat::Thread m_thread;
bool m_paused;
std::unique_ptr<IImageCompressor> m_compressor;
std::unique_ptr<uint8_t[]> m_buff;
uint32_t m_prevSize;
};
}
/*****************************************************************************
* Media Library
*****************************************************************************
* Copyright (C) 2015-2018 Hugo Beauzée-Luyssen, Videolabs, VideoLAN
*
* Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "VmemThumbnailer.h"
#include "Media.h"
#include "Thumbnail.h"
#include "utils/VLCInstance.h"
#include "metadata_services/vlc/Common.hpp"
#ifdef HAVE_JPEG
#include "imagecompressors/JpegCompressor.h"
#else
#error No image compressor available
#endif
namespace medialibrary
{
VmemThumbnailer::VmemThumbnailer( MediaLibraryPtr ml )
: m_ml( ml )
, m_prevSize( 0 )
{
#ifdef HAVE_JPEG
m_compressor.reset( new JpegCompressor );
#endif
}
bool VmemThumbnailer::generate( MediaPtr media, const std::string& mrl )
{
VLC::Media vlcMedia = VLC::Media( VLCInstance::get(), mrl,
VLC::Media::FromType::FromLocation );
vlcMedia.addOption( ":no-audio" );
vlcMedia.addOption( ":no-osd" );
vlcMedia.addOption( ":no-spu" );
vlcMedia.addOption( ":input-fast-seek" );
vlcMedia.addOption( ":avcodec-hw=none" );
vlcMedia.addOption( ":no-mkv-preload-local-dir" );
auto duration = vlcMedia.duration();
if ( duration > 0 )
{
std::ostringstream ss;
// Duration is in ms, start-time in seconds, and we're aiming at 1/4th of the media
ss << ":start-time=" << duration / 4000;
vlcMedia.addOption( ss.str() );
}
Task task{ media, mrl };
task.mp = VLC::MediaPlayer( vlcMedia );
setupVout( task );
auto res = MetadataCommon::startPlayback( vlcMedia, task.mp );
if ( res == false )
{
LOG_WARN( "Failed to generate ", mrl, " thumbnail: Can't start playback" );
return false;
}
if ( duration <= 0 )
{
// Seek ahead to have a significant preview
res = seekAhead( task );
if ( res == false )
{
LOG_WARN( "Failed to generate ", mrl, " thumbnail: Failed to seek ahead" );
return false;
}
}
return takeThumbnail( task );
}
bool VmemThumbnailer::seekAhead( Task& task )
{
float pos = .0f;
auto event = task.mp.eventManager().onPositionChanged([&task, &pos](float p) {
std::unique_lock<compat::Mutex> lock( task.mutex );
pos = p;
task.cond.notify_all();
});
auto success = false;
{
std::unique_lock<compat::Mutex> lock( task.mutex );
task.mp.setPosition( .4f );
success = task.cond.wait_for( lock, std::chrono::seconds( 3 ), [&pos]() {
return pos >= .1f;
});
}
// Since we're locking a mutex for each position changed, let's unregister ASAP
event->unregister();
return success;
}
void VmemThumbnailer::setupVout( Task& task )
{
task.mp.setVideoFormatCallbacks(
// Setup
[this, &task](char* chroma, unsigned int* width, unsigned int *height,
unsigned int *pitches, unsigned int *lines) {
strcpy( chroma, m_compressor->fourCC() );
const float inputAR = (float)*width / *height;
task.width = DesiredWidth;
task.height = (float)task.width / inputAR + 1;
if ( task.height < DesiredHeight )
{
// Avoid downscaling too much for really wide pictures
task.width = inputAR * DesiredHeight;
task.height = DesiredHeight;
}
auto size = task.width * task.height * m_compressor->bpp();
// If our buffer isn't enough anymore, reallocate a new one.
if ( size > m_prevSize )
{
m_buff.reset( new uint8_t[size] );
m_prevSize = size;
}
*width = task.width;
*height = task.height;
*pitches = task.width * m_compressor->bpp();
*lines = task.height;
return 1;
},
// Cleanup
nullptr);
task.mp.setVideoCallbacks(
// Lock
[this](void** pp_buff) {
*pp_buff = m_buff.get();
return nullptr;
},
//unlock
nullptr,
//display
[&task](void*) {
bool expected = true;
if ( task.thumbnailRequired.compare_exchange_strong( expected, false ) )
{
task.cond.notify_all();
}
}
);
}
bool VmemThumbnailer::takeThumbnail( Task& task )
{
// lock, signal that we want a thumbnail, and wait.
{
std::unique_lock<compat::Mutex> lock( task.mutex );
task.thumbnailRequired = true;
bool success = task.cond.wait_for( lock, std::chrono::seconds( 15 ), [&task]() {
// Keep waiting if the vmem thread hasn't restored m_thumbnailRequired to false
return task.thumbnailRequired == false;
});
if ( success == false )
{
LOG_WARN( "Timed out while computing ", task.mrl, " snapshot" );
return false;
}
}
task.mp.stop();
return compress( task );
}
bool VmemThumbnailer::compress( Task& task )
{
auto path = m_ml->thumbnailPath();
path += "/";
path += std::to_string( task.media->id() ) + "." + m_compressor->extension();
auto hOffset = task.width > DesiredWidth ? ( task.width - DesiredWidth ) / 2 : 0;
auto vOffset = task.height > DesiredHeight ? ( task.height - DesiredHeight ) / 2 : 0;