VLCThumbnailer.cpp 11.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*****************************************************************************
 * Media Library
 *****************************************************************************
 * Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
 *
 * 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.
 *****************************************************************************/

23
24
25
#include "VLCThumbnailer.h"

#include <cstring>
26
#ifdef WITH_JPEG
27
#include <jpeglib.h>
28
29
30
31
32
33
34
#if ( !defined(JPEG_LIB_VERSION_MAJOR) && !defined(JPEG_LIB_VERSION_MINOR) ) || \
    ( JPEG_LIB_VERSION_MAJOR <= 8 && JPEG_LIB_VERSION_MINOR < 4 )
//FIXME: I don't think we can expect this to work without VLC outputing BGR...
#define JPEG_COLORSPACE JCS_EXT_BGR
#else
#define JPEG_COLORSPACE JCS_RGB
#endif
35
36
37
#elif defined(WITH_EVAS)
#include <Evas_Engine_Buffer.h>
#endif
38
#include <setjmp.h>
39

40
#include "IMedia.h"
41
#include "logging/Logger.h"
42
#include "MediaLibrary.h"
43
44
45

VLCThumbnailer::VLCThumbnailer(const VLC::Instance &vlc)
    : m_instance( vlc )
46
47
48
#ifdef WITH_EVAS
    , m_canvas( nullptr, &evas_free )
#endif
49
    , m_snapshotRequired( false )
50
    , m_width( 0 )
51
    , m_height( 0 )
52
    , m_prevSize( 0 )
53
{
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#ifdef WITH_EVAS
    static int fakeBuffer;
    evas_init();
    auto method = evas_render_method_lookup("buffer");
    m_canvas.reset( evas_new() );
    if ( m_canvas == nullptr )
        throw std::runtime_error( "Failed to allocate canvas" );
    evas_output_method_set( m_canvas.get(), method );
    evas_output_size_set(m_canvas.get(), 1, 1 );
    evas_output_viewport_set( m_canvas.get(), 0, 0, 1, 1 );
    auto einfo = (Evas_Engine_Info_Buffer *)evas_engine_info_get( m_canvas.get() );
    einfo->info.depth_type = EVAS_ENGINE_BUFFER_DEPTH_ARGB32;
    einfo->info.dest_buffer = &fakeBuffer;
    einfo->info.dest_buffer_row_bytes = 4;
    einfo->info.use_color_key = 0;
    einfo->info.alpha_threshold = 0;
    einfo->info.func.new_update_region = NULL;
    einfo->info.func.free_update_region = NULL;
    evas_engine_info_set( m_canvas.get(), (Evas_Engine_Info *)einfo );
73
    m_cropBuffer.reset( new uint8_t[ DesiredWidth * DesiredHeight * Bpp ] );
74
75
76
77
78
79
80
81
#endif
}

VLCThumbnailer::~VLCThumbnailer()
{
#ifdef WITH_EVAS
    evas_shutdown();
#endif
82
83
}

84
bool VLCThumbnailer::initialize(IMetadataServiceCb *callback, MediaLibrary* ml)
85
86
87
88
89
90
91
92
93
94
95
96
{
    m_cb = callback;
    m_ml = ml;
    return true;
}

unsigned int VLCThumbnailer::priority() const
{
    // This needs to be lower than the VLCMetadataService, since we want to know the file type.
    return 50;
}

97
void VLCThumbnailer::run(std::shared_ptr<Media> file, void *data )
98
{
99
    if ( file->type() == IMedia::Type::UnknownType )
100
101
102
103
    {
        // If we don't know the file type yet, it actually looks more like a bug
        // since this should run after file type deduction, and not run in case
        // that step fails.
104
        m_cb->done( file, Status::Fatal, data );
105
        return;
106
    }
107
    else if ( file->type() != IMedia::Type::VideoType )
108
109
    {
        // There's no point in generating a thumbnail for a non-video file.
110
        m_cb->done( file, Status::Success, data );
111
        return;
112
    }
113
114
115
116
117
118
    else if ( file->snapshot().empty() == false )
    {
        LOG_INFO(file->snapshot(), " already has a snapshot" );
        m_cb->done( file, Status::Success, data );
        return;
    }
119
120
121
122
123
124
125
126

    VLC::Media media( m_instance, file->mrl(), VLC::Media::FromPath );
    media.addOption( ":no-audio" );
    VLC::MediaPlayer mp( media );

    setupVout( mp );

    if ( startPlayback( file, mp, data ) == false )
127
        return;
128
129
130

    // Seek ahead to have a significant preview
    if ( seekAhead( file, mp, data ) == false )
131
        return;
132

133
    takeSnapshot( file, mp, data );
134
135
}

136
bool VLCThumbnailer::startPlayback(std::shared_ptr<Media> file, VLC::MediaPlayer &mp, void* data )
137
138
139
140
141
{
    bool failed = true;

    std::unique_lock<std::mutex> lock( m_mutex );

142
    mp.eventManager().onPlaying([this, &failed]() {
143
144
145
146
147
148
149
150
151
        std::unique_lock<std::mutex> lock( m_mutex );
        failed = false;
        m_cond.notify_all();
    });
    mp.eventManager().onEncounteredError([this]() {
        std::unique_lock<std::mutex> lock( m_mutex );
        m_cond.notify_all();
    });
    mp.play();
152
153
    bool success = m_cond.wait_for( lock, std::chrono::seconds( 3 ), [&mp]() {
        return mp.state() == libvlc_Playing || mp.state() == libvlc_Error;
154
155
156
157
    });
    if ( success == false || failed == true )
    {
        // In case of timeout or error, don't go any further
158
        m_cb->done( file, Status::Error, data );
159
160
161
162
163
        return false;
    }
    return true;
}

164
bool VLCThumbnailer::seekAhead(std::shared_ptr<Media> file, VLC::MediaPlayer& mp, void* data )
165
166
167
168
169
170
171
172
{
    std::unique_lock<std::mutex> lock( m_mutex );
    float pos = .0f;
    auto event = mp.eventManager().onPositionChanged([this, &pos](float p) {
        std::unique_lock<std::mutex> lock( m_mutex );
        pos = p;
        m_cond.notify_all();
    });
173
    mp.setPosition( .4f );
174
175
176
177
178
179
180
    bool success = m_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 )
    {
181
        m_cb->done( file, Status::Error, data );
182
183
184
185
186
        return false;
    }
    return true;
}

187
188
189
190
191
void VLCThumbnailer::setupVout( VLC::MediaPlayer& mp )
{
    mp.setVideoFormatCallbacks(
        // Setup
        [this, &mp](char* chroma, unsigned int* width, unsigned int *height, unsigned int *pitches, unsigned int *lines) {
192
            strcpy( chroma, VLC_FOURCC );
193
194
195

            const float inputAR = (float)*width / *height;

196
197
198
199
200
201
202
203
204
            m_width = DesiredWidth;
            m_height = (float)m_width / inputAR + 1;
            if ( m_height < DesiredHeight )
            {
                // Avoid downscaling too much for really wide pictures
                m_width = inputAR * DesiredHeight;
                m_height = DesiredHeight;
            }
            auto size = m_width * m_height * Bpp;
205
            // If our buffer isn't enough anymore, reallocate a new one.
206
            if ( size > m_prevSize )
207
            {
208
209
                m_buff.reset( new uint8_t[size] );
                m_prevSize = size;
210
            }
211
            *width = m_width;
212
            *height = m_height;
213
            *pitches = m_width * Bpp;
214
215
216
217
218
219
220
221
222
223
224
225
226
            *lines = m_height;
            return 1;
        },
        // Cleanup
        nullptr);
    mp.setVideoCallbacks(
        // Lock
        [this](void** pp_buff) {
            *pp_buff = m_buff.get();
            return nullptr;
        },
        //unlock
        [this](void*, void*const*) {
227
228
            bool expected = true;
            if ( m_snapshotRequired.compare_exchange_strong( expected, false ) )
229
230
231
232
233
234
235
236
237
238
            {
                m_cond.notify_all();
            }
        }
        ,
        //display
        nullptr
    );
}

239
bool VLCThumbnailer::takeSnapshot(std::shared_ptr<Media> file, VLC::MediaPlayer &mp, void *data)
240
{
241
242
243
244
245
246
    // lock, signal that we want a snapshot, and wait.
    {
        std::unique_lock<std::mutex> lock( m_mutex );
        m_snapshotRequired = true;
        bool success = m_cond.wait_for( lock, std::chrono::seconds( 3 ), [this]() {
            // Keep waiting if the vmem thread hasn't restored m_snapshotRequired to false
247
            return m_snapshotRequired == false;
248
249
250
        });
        if ( success == false )
        {
251
            m_cb->done( file, Status::Error, data );
252
253
254
255
            return false;
        }
    }
    mp.stop();
256
    return compress( file, data );
257
258
}

259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#ifdef WITH_JPEG

struct jpegError : public jpeg_error_mgr
{
    jmp_buf buff;
    char message[JMSG_LENGTH_MAX];

    static void jpegErrorHandler(j_common_ptr common)
    {
        auto error = reinterpret_cast<jpegError*>(common->err);
        (*error->format_message)( common, error->message );
        longjmp(error->buff, 1);
    }
};

#endif

276
bool VLCThumbnailer::compress( std::shared_ptr<Media> file, void *data )
277
278
279
{
    auto path = m_ml->snapshotPath();
    path += "/";
280
281
282
283
284
285
    path += std::to_string( file->id() ) +
#ifdef WITH_EVAS
            ".png";
#else
            ".jpg";
#endif
286

287
288
289
290
    auto hOffset = m_width > DesiredWidth ? ( m_width - DesiredWidth ) / 2 : 0;
    auto vOffset = m_height > DesiredHeight ? ( m_height - DesiredHeight ) / 2 : 0;
    const auto stride = m_width * Bpp;

291
#ifdef WITH_JPEG
292
293
294
295
296
297
298
    //FIXME: Abstract this away, though libjpeg requires a FILE*...
    auto fOut = fopen(path.c_str(), "wb");
    // ensure we always close the file.
    auto fOutPtr = std::unique_ptr<FILE, int(*)(FILE*)>( fOut, &fclose );
    if ( fOut == nullptr )
    {
        LOG_ERROR("Failed to open snapshot file ", path);
299
        m_cb->done( file, Status::Error, data );
300
301
        return false;
    }
302
303
304
305
306
307
308
309
310
311
312
313
314
315

    jpeg_compress_struct compInfo;
    JSAMPROW row_pointer[1];

    //libjpeg's default error handling is to call exit(), which would
    //be slightly problematic...
    jpegError err;
    compInfo.err = jpeg_std_error(&err);
    err.error_exit = jpegError::jpegErrorHandler;

    if ( setjmp( err.buff ) )
    {
        LOG_ERROR("JPEG failure: ", err.message);
        jpeg_destroy_compress(&compInfo);
316
        m_cb->done( file, Status::Error, data );
317
318
319
        return false;
    }

320
    jpeg_create_compress(&compInfo);
321
322
    jpeg_stdio_dest(&compInfo, fOut);

323
324
    compInfo.image_width = DesiredWidth;
    compInfo.image_height = DesiredHeight;
325
    compInfo.input_components = Bpp;
326
    compInfo.in_color_space = JPEG_COLORSPACE;
327
328
329
330
331
    jpeg_set_defaults( &compInfo );
    jpeg_set_quality( &compInfo, 85, TRUE );

    jpeg_start_compress( &compInfo, TRUE );

332
333
334
335
    while (compInfo.next_scanline < DesiredHeight)
    {
        row_pointer[0] = &m_buff[(compInfo.next_scanline + vOffset ) * stride + hOffset];
        jpeg_write_scanlines(&compInfo, row_pointer, 1);
336
337
338
    }
    jpeg_finish_compress(&compInfo);
    jpeg_destroy_compress(&compInfo);
339
340
341
342
343
#elif defined(WITH_EVAS)
    auto evas_obj = std::unique_ptr<Evas_Object, void(*)(Evas_Object*)>( evas_object_image_add( m_canvas.get() ), evas_object_del );
    if ( evas_obj == nullptr )
        return false;

344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
    uint8_t *p_buff = m_buff.get();
    if ( DesiredWidth != m_width )
    {
        p_buff += vOffset * stride;
        for ( auto y = 0u; y < DesiredHeight; ++y )
        {
            memcpy( m_cropBuffer.get() + y * DesiredWidth * Bpp, p_buff + (hOffset * Bpp), DesiredWidth * Bpp );
            p_buff += stride;
        }
        vOffset = 0;
        p_buff = m_cropBuffer.get();
    }

    evas_object_image_colorspace_set( evas_obj.get(), EVAS_COLORSPACE_ARGB8888 );
    evas_object_image_size_set( evas_obj.get(), DesiredWidth, DesiredHeight );
    evas_object_image_data_set( evas_obj.get(), p_buff + vOffset * stride );
360
    evas_object_image_save( evas_obj.get(), path.c_str(), NULL, "quality=100 compress=9");
361
362
363
#else
#error FIXME
#endif
364
365

    file->setSnapshot( path );
366
    m_cb->done( file, Status::Success, data );
367
    return true;
368
}