VLCThumbnailer.cpp 10.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
#elif defined(WITH_EVAS)
#include <Evas_Engine_Buffer.h>
#endif
31
#include <setjmp.h>
32

33
#include "IMedia.h"
34
#include "logging/Logger.h"
35
#include "MediaLibrary.h"
36
37
38

VLCThumbnailer::VLCThumbnailer(const VLC::Instance &vlc)
    : m_instance( vlc )
39
40
41
#ifdef WITH_EVAS
    , m_canvas( nullptr, &evas_free )
#endif
42
43
    , m_snapshotRequired( false )
    , m_height( 0 )
44
    , m_prevSize( 0 )
45
{
46
47
48
49
50
51
52
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 );
#endif
}

VLCThumbnailer::~VLCThumbnailer()
{
#ifdef WITH_EVAS
    evas_shutdown();
#endif
73
74
}

75
bool VLCThumbnailer::initialize(IMetadataServiceCb *callback, MediaLibrary* ml)
76
77
78
79
80
81
82
83
84
85
86
87
{
    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;
}

88
void VLCThumbnailer::run(std::shared_ptr<Media> file, void *data )
89
{
90
    if ( file->type() == IMedia::Type::UnknownType )
91
92
93
94
    {
        // 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.
95
        m_cb->done( file, Status::Fatal, data );
96
        return;
97
    }
98
    else if ( file->type() != IMedia::Type::VideoType )
99
100
    {
        // There's no point in generating a thumbnail for a non-video file.
101
        m_cb->done( file, Status::Success, data );
102
        return;
103
    }
104
105
106
107
108
109
    else if ( file->snapshot().empty() == false )
    {
        LOG_INFO(file->snapshot(), " already has a snapshot" );
        m_cb->done( file, Status::Success, data );
        return;
    }
110
111
112
113
114
115
116
117

    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 )
118
        return;
119
120
121

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

124
    takeSnapshot( file, mp, data );
125
126
}

127
bool VLCThumbnailer::startPlayback(std::shared_ptr<Media> file, VLC::MediaPlayer &mp, void* data )
128
129
130
131
132
{
    bool failed = true;

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

133
    mp.eventManager().onPlaying([this, &failed]() {
134
135
136
137
138
139
140
141
142
        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();
143
144
    bool success = m_cond.wait_for( lock, std::chrono::seconds( 3 ), [&mp]() {
        return mp.state() == libvlc_Playing || mp.state() == libvlc_Error;
145
146
147
148
    });
    if ( success == false || failed == true )
    {
        // In case of timeout or error, don't go any further
149
        m_cb->done( file, Status::Error, data );
150
151
152
153
154
        return false;
    }
    return true;
}

155
bool VLCThumbnailer::seekAhead(std::shared_ptr<Media> file, VLC::MediaPlayer& mp, void* data )
156
157
158
159
160
161
162
163
{
    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();
    });
164
    mp.setPosition( .4f );
165
166
167
168
169
170
171
    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 )
    {
172
        m_cb->done( file, Status::Error, data );
173
174
175
176
177
        return false;
    }
    return true;
}

178
179
180
181
182
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) {
183
            strcpy( chroma, "RV32" );
184
185
186
187
188

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

            *width = Width;
            m_height = (float)Width / inputAR + 1;
189
            auto size = Width * m_height * Bpp;
190
            // If our buffer isn't enough anymore, reallocate a new one.
191
            if ( size > m_prevSize )
192
            {
193
194
                m_buff.reset( new uint8_t[size] );
                m_prevSize = size;
195
            }
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
            *height = m_height;
            *pitches = Width * Bpp;
            *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*) {
211
212
            bool expected = true;
            if ( m_snapshotRequired.compare_exchange_strong( expected, false ) )
213
214
215
216
217
218
219
220
221
222
            {
                m_cond.notify_all();
            }
        }
        ,
        //display
        nullptr
    );
}

223
bool VLCThumbnailer::takeSnapshot(std::shared_ptr<Media> file, VLC::MediaPlayer &mp, void *data)
224
{
225
226
227
228
229
230
    // 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
231
            return m_snapshotRequired == false;
232
233
234
        });
        if ( success == false )
        {
235
            m_cb->done( file, Status::Error, data );
236
237
238
239
            return false;
        }
    }
    mp.stop();
240
    return compress( file, data );
241
242
}

243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#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

260
bool VLCThumbnailer::compress( std::shared_ptr<Media> file, void *data )
261
262
263
{
    auto path = m_ml->snapshotPath();
    path += "/";
264
265
266
267
268
269
    path += std::to_string( file->id() ) +
#ifdef WITH_EVAS
            ".png";
#else
            ".jpg";
#endif
270

271
#ifdef WITH_JPEG
272
273
274
275
276
277
278
    //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);
279
        m_cb->done( file, Status::Error, data );
280
281
        return false;
    }
282
283
284
285
286
287
288
289
290
291
292
293
294
295

    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);
296
        m_cb->done( file, Status::Error, data );
297
298
299
        return false;
    }

300
    jpeg_create_compress(&compInfo);
301
302
303
304
305
    jpeg_stdio_dest(&compInfo, fOut);

    compInfo.image_width = Width;
    compInfo.image_height = m_height;
    compInfo.input_components = Bpp;
306
307
308
#if ( !defined(JPEG_LIB_VERSION_MAJOR) && !defined(JPEG_LIB_VERSION_MINOR) ) || \
    ( JPEG_LIB_VERSION_MAJOR <= 8 && JPEG_LIB_VERSION_MINOR < 4 )
    compInfo.in_color_space = JCS_EXT_BGR;
309
310
311
#else
    compInfo.in_color_space = JCS_RGB;
#endif
312
313
314
315
316
317
318
319
    jpeg_set_defaults( &compInfo );
    jpeg_set_quality( &compInfo, 85, TRUE );

    jpeg_start_compress( &compInfo, TRUE );

    auto stride = compInfo.image_width * Bpp;

    while (compInfo.next_scanline < compInfo.image_height) {
320
      row_pointer[0] = &m_buff[compInfo.next_scanline * stride];
321
322
323
324
      jpeg_write_scanlines(&compInfo, row_pointer, 1);
    }
    jpeg_finish_compress(&compInfo);
    jpeg_destroy_compress(&compInfo);
325
326
327
328
329
330
#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;
    evas_object_image_colorspace_set( evas_obj.get(), EVAS_COLORSPACE_ARGB8888 );
    evas_object_image_size_set( evas_obj.get(), Width, m_height );
331
    evas_object_image_data_set( evas_obj.get(), m_buff.get() );
332
333

    evas_object_image_save( evas_obj.get(), path.c_str(), NULL, "quality=100 compress=9");
334
335
336
#else
#error FIXME
#endif
337
338

    file->setSnapshot( path );
339
    m_cb->done( file, Status::Success, data );
340
    return true;
341
}