Commit 9ae0d60d authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Use lazy loading for thumbnails

It is now up to the user to request thumbnails generation.
It allows lazy loading, and prevents us from potentially generating
thousands of thumbnails for nothing when indexing an entire network
drive.
parent 2d97d88f
......@@ -193,6 +193,13 @@ public:
* @param isIdle true when all background tasks are idle, false otherwise
*/
virtual void onBackgroundTasksIdleChanged( bool isIdle ) = 0;
/**
* @brief onMediaThumbnailReady Called when a thumbnail generation completed.
* @param media The media for which a thumbnail was generated
* @param success true if the thumbnail was generated, false if the generation failed
*/
virtual void onMediaThumbnailReady( MediaPtr media, bool success ) = 0;
};
class IMediaLibrary
......@@ -375,6 +382,15 @@ class IMediaLibrary
* as invalid the moment this method returns.
*/
virtual void forceRescan() = 0;
/**
* \brief requestThumbnail Queues a thumbnail generation request for
* this media, to be run asynchronously.
* Upon completion (successful or not) IMediaLibraryCb::onMediaThumbnailReady
* will be called.
*/
virtual void requestThumbnail( MediaPtr media ) = 0;
};
}
......
......@@ -325,6 +325,7 @@ bool MediaLibrary::start()
refreshDevices( *fsFactory );
startDiscoverer();
startParser();
startThumbnailer();
return true;
}
......@@ -717,10 +718,8 @@ void MediaLibrary::startParser()
auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService );
auto metadataService = std::unique_ptr<MetadataParser>( new MetadataParser );
auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer );
m_parser->addService( std::move( vlcService ) );
m_parser->addService( std::move( metadataService ) );
m_parser->addService( std::move( thumbnailerService ) );
m_parser->start();
}
......@@ -741,6 +740,11 @@ void MediaLibrary::startDeletionNotifier()
m_modificationNotifier->start();
}
void MediaLibrary::startThumbnailer()
{
m_thumbnailer = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer( this ) );
}
void MediaLibrary::addLocalFsFactory()
{
m_fsFactories.insert( begin( m_fsFactories ), std::make_shared<factory::FileSystemFactory>( m_deviceLister ) );
......@@ -1225,6 +1229,11 @@ void MediaLibrary::forceRescan()
}
}
void MediaLibrary::requestThumbnail( MediaPtr media )
{
m_thumbnailer->requestThumbnail( media );
}
bool MediaLibrary::onDevicePlugged( const std::string& uuid, const std::string& mountpoint )
{
auto currentDevice = Device::fromUuid( this, uuid );
......
......@@ -36,6 +36,7 @@ class ModificationNotifier;
class DiscovererWorker;
class Parser;
class ParserService;
class VLCThumbnailer;
class Album;
class Artist;
......@@ -152,6 +153,8 @@ class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
virtual void forceRescan() override;
virtual void requestThumbnail( MediaPtr media );
static bool isExtensionSupported( const char* ext );
protected:
......@@ -163,6 +166,7 @@ class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
virtual void startParser();
virtual void startDiscoverer();
virtual void startDeletionNotifier();
virtual void startThumbnailer();
private:
bool recreateDatabase( const std::string& dbPath );
......@@ -213,6 +217,7 @@ class MediaLibrary : public IMediaLibrary, public IDeviceListerCb
bool m_initialized;
std::atomic_bool m_discovererIdle;
std::atomic_bool m_parserIdle;
std::unique_ptr<VLCThumbnailer> m_thumbnailer;
};
}
......
......@@ -208,10 +208,6 @@ parser::Task::Status MetadataParser::run( parser::Task& task )
return parser::Task::Status::Fatal;
task.markStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
// Save ourselves from the useless processing of a thumbnail later if
// we're analyzing an audio file
if ( isAudio == true )
task.markStepCompleted( parser::Task::ParserStep::Thumbnailer );
if ( task.saveParserStep() == false )
return parser::Task::Status::Fatal;
m_notifier->notifyMediaCreation( task.media );
......
......@@ -48,11 +48,9 @@
namespace medialibrary
{
VLCThumbnailer::VLCThumbnailer()
: m_instance( VLCInstance::get() )
, m_thumbnailRequired( false )
, m_width( 0 )
, m_height( 0 )
VLCThumbnailer::VLCThumbnailer( MediaLibraryPtr ml )
: m_ml( ml )
, m_run( false )
, m_prevSize( 0 )
{
#ifdef HAVE_JPEG
......@@ -62,139 +60,183 @@ VLCThumbnailer::VLCThumbnailer()
#endif
}
bool VLCThumbnailer::initialize()
VLCThumbnailer::~VLCThumbnailer()
{
return true;
stop();
}
bool VLCThumbnailer::isCompleted( const parser::Task& task ) const
void VLCThumbnailer::requestThumbnail( MediaPtr media )
{
return task.isStepCompleted( parser::Task::ParserStep::Thumbnailer );
{
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 ) );
}
if ( m_thread.get_id() == compat::Thread::id{} )
{
m_run = true;
m_thread = compat::Thread( &VLCThumbnailer::run, this );
}
}
parser::Task::Status VLCThumbnailer::run( parser::Task& task )
void VLCThumbnailer::run()
{
assert( task.media->type() != Media::Type::Audio );
LOG_INFO( "Starting thumbnailer thread" );
while ( m_run == true )
{
std::unique_ptr<Task> task;
{
std::unique_lock<compat::Mutex> lock( m_mutex );
if ( m_tasks.size() == 0 )
{
m_cond.wait( lock, [this]() { return m_tasks.size() > 0 || m_run == false; } );
if ( m_run == false )
break;
}
task = std::move( m_tasks.front() );
m_tasks.pop();
}
bool res = generateThumbnail( *task );
m_ml->getCb()->onMediaThumbnailReady( task->media, res );
}
LOG_INFO( "Exiting thumbnailer thread" );
}
auto media = task.media.get();
auto file = task.file.get();
void VLCThumbnailer::stop()
{
bool running = true;
if ( m_run.compare_exchange_strong( running, false ) )
{
{
std::unique_lock<compat::Mutex> lock( m_mutex );
while ( m_tasks.empty() == false )
m_tasks.pop();
}
m_cond.notify_all();
m_thread.join();
}
}
LOG_INFO( "Generating ", file->mrl(), " thumbnail..." );
bool VLCThumbnailer::generateThumbnail( Task& task )
{
assert( task.media->type() != Media::Type::Audio );
if ( task.vlcMedia.isValid() == false )
auto files = task.media->files();
if ( files.empty() == true )
{
task.vlcMedia = VLC::Media( m_instance, file->mrl(), VLC::Media::FromType::FromLocation );
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..." );
task.vlcMedia.addOption( ":no-audio" );
task.vlcMedia.addOption( ":no-osd" );
task.vlcMedia.addOption( ":no-spu" );
task.vlcMedia.addOption( ":input-fast-seek" );
task.vlcMedia.addOption( ":avcodec-hw=none" );
task.vlcMedia.addOption( ":no-mkv-preload-local-dir" );
auto duration = task.vlcMedia.duration();
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;
task.vlcMedia.addOption( ss.str() );
vlcMedia.addOption( ss.str() );
}
VLC::MediaPlayer mp( task.vlcMedia );
task.mp = VLC::MediaPlayer( vlcMedia );
setupVout( mp );
setupVout( task );
if ( MetadataCommon::startPlayback( task.vlcMedia, mp ) == false )
auto res = MetadataCommon::startPlayback( vlcMedia, task.mp );
if ( res == false )
{
LOG_WARN( "Failed to generate ", file->mrl(), " thumbnail: Can't start playback" );
return parser::Task::Status::Fatal;
LOG_WARN( "Failed to generate ", task.mrl, " thumbnail: Can't start playback" );
return false;
}
if ( duration <= 0 )
{
// Seek ahead to have a significant preview
auto res = seekAhead( mp );
if ( res != parser::Task::Status::Success )
res = seekAhead( task );
if ( res == false )
{
LOG_WARN( "Failed to generate ", file->mrl(), " thumbnail: Failed to seek ahead" );
return res;
LOG_WARN( "Failed to generate ", task.mrl, " thumbnail: Failed to seek ahead" );
return false;
}
}
auto res = takeThumbnail( media, file, mp );
if ( res != parser::Task::Status::Success )
return res;
LOG_INFO( "Done generating ", file->mrl(), " thumbnail" );
if ( task.file->isDeleted() == true || task.media->isDeleted() == true )
return parser::Task::Status::Fatal;
task.markStepCompleted( parser::Task::ParserStep::Thumbnailer );
m_notifier->notifyMediaModification( task.media );
auto t = m_ml->getConn()->newTransaction();
if ( media->save() == false || task.saveParserStep() == false )
return parser::Task::Status::Fatal;
t->commit();
return parser::Task::Status::Success;
res = takeThumbnail( task );
if ( res == false )
return false;
LOG_INFO( "Done generating ", task.mrl, " thumbnail" );
m_ml->getNotifier()->notifyMediaModification( task.media );
return true;
}
parser::Task::Status VLCThumbnailer::seekAhead( VLC::MediaPlayer& mp )
bool VLCThumbnailer::seekAhead( Task& task )
{
float pos = .0f;
auto event = mp.eventManager().onPositionChanged([this, &pos](float p) {
std::unique_lock<compat::Mutex> lock( m_mutex );
auto event = task.mp.eventManager().onPositionChanged([&task, &pos](float p) {
std::unique_lock<compat::Mutex> lock( task.mutex );
pos = p;
m_cond.notify_all();
task.cond.notify_all();
});
auto success = false;
{
std::unique_lock<compat::Mutex> lock( m_mutex );
mp.setPosition( .4f );
success = m_cond.wait_for( lock, std::chrono::seconds( 3 ), [&pos]() {
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();
if ( success == false )
return parser::Task::Status::Fatal;
return parser::Task::Status::Success;
return success;
}
void VLCThumbnailer::setupVout( VLC::MediaPlayer& mp )
void VLCThumbnailer::setupVout( Task& task )
{
mp.setVideoFormatCallbacks(
task.mp.setVideoFormatCallbacks(
// Setup
[this, &mp](char* chroma, unsigned int* width, unsigned int *height, unsigned int *pitches, unsigned int *lines) {
[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;
m_width = DesiredWidth;
m_height = (float)m_width / inputAR + 1;
if ( m_height < DesiredHeight )
task.width = DesiredWidth;
task.height = (float)task.width / inputAR + 1;
if ( task.height < DesiredHeight )
{
// Avoid downscaling too much for really wide pictures
m_width = inputAR * DesiredHeight;
m_height = DesiredHeight;
task.width = inputAR * DesiredHeight;
task.height = DesiredHeight;
}
auto size = m_width * m_height * m_compressor->bpp();
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 = m_width;
*height = m_height;
*pitches = m_width * m_compressor->bpp();
*lines = m_height;
*width = task.width;
*height = task.height;
*pitches = task.width * m_compressor->bpp();
*lines = task.height;
return 1;
},
// Cleanup
nullptr);
mp.setVideoCallbacks(
task.mp.setVideoCallbacks(
// Lock
[this](void** pp_buff) {
*pp_buff = m_buff.get();
......@@ -204,61 +246,59 @@ void VLCThumbnailer::setupVout( VLC::MediaPlayer& mp )
nullptr,
//display
[this](void*) {
[this, &task](void*) {
bool expected = true;
if ( m_thumbnailRequired.compare_exchange_strong( expected, false ) )
if ( task.thumbnailRequired.compare_exchange_strong( expected, false ) )
{
m_cond.notify_all();
task.cond.notify_all();
}
}
);
}
parser::Task::Status VLCThumbnailer::takeThumbnail( Media* media, File* file, VLC::MediaPlayer &mp )
bool VLCThumbnailer::takeThumbnail( Task& task )
{
// lock, signal that we want a thumbnail, and wait.
{
std::unique_lock<compat::Mutex> lock( m_mutex );
m_thumbnailRequired = true;
bool success = m_cond.wait_for( lock, std::chrono::seconds( 15 ), [this]() {
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 m_thumbnailRequired == false;
return task.thumbnailRequired == false;
});
if ( success == false )
{
LOG_WARN( "Timed out while computing ", file->mrl(), " snapshot" );
return parser::Task::Status::Fatal;
LOG_WARN( "Timed out while computing ", task.mrl, " snapshot" );
return false;
}
}
mp.stop();
return compress( media, file );
task.mp.stop();
return compress( task );
}
parser::Task::Status VLCThumbnailer::compress( Media* media, File* file )
bool VLCThumbnailer::compress( Task& task )
{
auto path = m_ml->thumbnailPath();
path += "/";
path += std::to_string( media->id() ) + "." + m_compressor->extension();
path += std::to_string( task.media->id() ) + "." + m_compressor->extension();
auto hOffset = m_width > DesiredWidth ? ( m_width - DesiredWidth ) / 2 : 0;
auto vOffset = m_height > DesiredHeight ? ( m_height - DesiredHeight ) / 2 : 0;
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, m_width, m_height, DesiredWidth, DesiredHeight,
hOffset, vOffset ) == false )
return parser::Task::Status::Fatal;
if ( m_compressor->compress( m_buff.get(), path, task.width, task.height,
DesiredWidth, DesiredHeight, hOffset, vOffset ) == false )
return false;
media->setThumbnailCached( path );
return parser::Task::Status::Success;
}
const char*VLCThumbnailer::name() const
{
return "Thumbnailer";
task.media->setThumbnail( path );
return true;
}
uint8_t VLCThumbnailer::nbThreads() const
VLCThumbnailer::Task::Task( MediaPtr m )
: media( std::move( m ) )
, width( 0 )
, height( 0 )
, thumbnailRequired( false )
{
return 1;
}
}
......@@ -32,23 +32,36 @@
namespace medialibrary
{
class VLCThumbnailer : public ParserService
class VLCThumbnailer
{
struct Task
{
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:
explicit VLCThumbnailer();
virtual ~VLCThumbnailer() = default;
virtual parser::Task::Status run( parser::Task& task ) override;
virtual bool initialize() override;
virtual bool isCompleted( const parser::Task& task ) const override;
explicit VLCThumbnailer( MediaLibraryPtr ml );
virtual ~VLCThumbnailer();
void requestThumbnail( MediaPtr media );
private:
parser::Task::Status seekAhead( VLC::MediaPlayer &mp );
void setupVout( VLC::MediaPlayer &mp );
parser::Task::Status takeThumbnail( Media* media, File* file, VLC::MediaPlayer &mp );
parser::Task::Status compress( Media* media, File* file );
void run();
void stop();
virtual const char* name() const override;
virtual uint8_t nbThreads() const override;
bool generateThumbnail( Task& task );
bool seekAhead( Task& task );
void setupVout( Task& task );
bool takeThumbnail( Task& task );
bool compress( Task& task );
private:
// Force a base width, let height be computed depending on A/R
......@@ -56,15 +69,15 @@ private:
static const uint32_t DesiredHeight = 200; // Aim for a 16:10 thumbnail
private:
VLC::Instance m_instance;
MediaLibraryPtr m_ml;
compat::Mutex m_mutex;
compat::ConditionVariable m_cond;
std::queue<std::unique_ptr<Task>> m_tasks;
std::atomic_bool m_run;
compat::Thread m_thread;
std::unique_ptr<IImageCompressor> m_compressor;
// Per thumbnail variables
std::unique_ptr<uint8_t[]> m_buff;
std::atomic_bool m_thumbnailRequired;
uint32_t m_width;
uint32_t m_height;
uint32_t m_prevSize;
};
......
......@@ -85,9 +85,8 @@ public:
None = 0,
MetadataExtraction = 1,
MetadataAnalysis = 2,
Thumbnailer = 4,
Completed = 1 | 2 | 4,
Completed = 1 | 2,
};
/*
......
......@@ -55,6 +55,7 @@ class NoopCallback : public IMediaLibraryCb
virtual void onEntryPointBanned( const std::string&, bool ) override {}
virtual void onEntryPointUnbanned( const std::string&, bool ) override {}
virtual void onBackgroundTasksIdleChanged( bool ) override {}
virtual void onMediaThumbnailReady( MediaPtr, bool ) override {}
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment