| 21 | class NextThumbnailEvent : public QEvent |
| 22 | { |
| 23 | public: |
| 24 | NextThumbnailEvent(ThumbnailCache *c) : QEvent(kEventType), m_cache(c) {} |
| 25 | static Type kEventType; |
| 26 | ThumbnailCache *m_cache; |
| 27 | }; |
| 28 | |
| 29 | QEvent::Type NextThumbnailEvent::kEventType = |
| 30 | (QEvent::Type) QEvent::registerEventType(); |
| 31 | |
| 32 | ThumbnailCache::ThumbnailCache(PlayerContext *origCtx, |
| 33 | const QVector<ThumbnailLayout> &images) : |
| 34 | MThread("Thumbnail loader") |
| 35 | { |
| 36 | m_ctx = NULL; |
| 37 | m_origCtx = origCtx; |
| 38 | m_images = images; |
| 39 | m_isEventOutstanding = false; |
| 40 | m_totalDataBytes = 0; |
| 41 | m_maxDataBytes = gCoreContext->GetNumSetting("UIImageCacheSize", 20) |
| 42 | * 1024 * 1024 * 8; |
| 43 | m_keepRunning = true; |
| 44 | m_timestamp = 0; |
| 45 | } |
| 46 | |
| 47 | ThumbnailCache::~ThumbnailCache(void) |
| 48 | { |
| 49 | m_keepRunning = false; |
| 50 | wait(); |
| 51 | if (m_ctx) |
| 52 | delete m_ctx; |
| 53 | QHash<uint64_t, ThumbnailCacheEntry>::iterator iter = m_cache.begin(); |
| 54 | for (; iter != m_cache.end(); ++iter) |
| 55 | { |
| 56 | delete (*iter).m_image; |
| 57 | delete (*iter).m_data; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | void ThumbnailCache::run(void) |
| 62 | { |
| 63 | RunProlog(); |
| 64 | |
| 65 | m_ctx = new PlayerContext("Cutlist editor thumbnail"); |
| 66 | m_rbuf = RingBuffer::Create(m_origCtx->buffer->GetFilename(), false); |
| 67 | m_ctx->SetRingBuffer(m_rbuf); |
| 68 | m_ctx->SetPlayingInfo(m_origCtx->playingInfo); |
| 69 | m_player = |
| 70 | new MythPlayer((PlayerFlags)(kAudioMuted | kVideoIsNull |
| 71 | /* | kDecodeAllowGPU*/)); |
| 72 | m_ctx->SetPlayer(m_player); |
| 73 | m_ctx->player->SetPlayerInfo(NULL, NULL, true, m_ctx); |
| 74 | |
| 75 | while (m_keepRunning) |
| 76 | { |
| 77 | bool isEmpty; |
| 78 | uint64_t target = 0; |
| 79 | { |
| 80 | QMutexLocker l(&m_cacheLock); |
| 81 | isEmpty = m_loadQueue.isEmpty(); |
| 82 | if (!isEmpty) |
| 83 | target = m_loadQueue.front(); |
| 84 | } |
| 85 | if (!isEmpty) |
| 86 | { |
| 87 | int bufflen, vw, vh; |
| 88 | float ar; |
| 89 | char *grab = |
| 90 | m_ctx->player->GetScreenGrabAtFrame(target, true, bufflen, |
| 91 | vw, vh, ar); |
| 92 | { |
| 93 | QMutexLocker l(&m_cacheLock); |
| 94 | int length = vw * vh * 4; |
| 95 | m_totalDataBytes += length; |
| 96 | QImage *qi = new QImage((unsigned char *)grab, vw, vh, |
| 97 | QImage::Format_RGB32); |
| 98 | m_cache[target] = ThumbnailCacheEntry(qi, (unsigned char *)grab, |
| 99 | length, m_timestamp++); |
| 100 | m_loadQueue.removeAll(target); |
| 101 | isEmpty = m_loadQueue.isEmpty(); |
| 102 | } |
| 103 | LOG(VB_PLAYBACK, LOG_DEBUG, |
| 104 | QString("Grabbed frame %1, %2x%3").arg(target).arg(vw).arg(vh)); |
| 105 | } |
| 106 | if (isEmpty) |
| 107 | usleep(1000); |
| 108 | } |
| 109 | RunEpilog(); |
| 110 | } |
| 111 | |
| 112 | void ThumbnailCache::customEvent(QEvent *event) |
| 113 | { |
| 114 | if (event->type() == NextThumbnailEvent::kEventType) |
| 115 | { |
| 116 | NextThumbnailEvent *nte = dynamic_cast<NextThumbnailEvent*>(event); |
| 117 | nte->m_cache->m_isEventOutstanding = false; |
| 118 | nte->m_cache->Load(); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | void ThumbnailCache::Load(void) |
| 123 | { |
| 124 | if (m_isEventOutstanding) |
| 125 | return; |
| 126 | bool moreToLoad = false; |
| 127 | { |
| 128 | QMutexLocker l(&m_cacheLock); |
| 129 | // Load all images that are cached. |
| 130 | for (int i = 0; i < m_images.size(); ++i) |
| 131 | { |
| 132 | if (!m_displayed.contains(i)) |
| 133 | { |
| 134 | int offset = m_seekamount * m_images[i].m_steps; |
| 135 | uint64_t target = m_baseFrame + offset; |
| 136 | if (offset < 0 && target > m_baseFrame) |
| 137 | target = 0; // handle underflow |
| 138 | if (m_cache.contains(target)) |
| 139 | { |
| 140 | MythUIImage *imageType = m_images[i].m_image; |
| 141 | MythImage *mi = new MythImage(imageType->GetPainter()); |
| 142 | QImage *qi = m_cache[target].m_image; |
| 143 | m_cache[target].m_timestamp = m_timestamp++; |
| 144 | mi->Assign(*qi); |
| 145 | imageType->SetImage(mi); |
| 146 | m_displayed += i; |
| 147 | } |
| 148 | else |
| 149 | { |
| 150 | moreToLoad = true; |
| 151 | if (!m_loadQueue.contains(target)) |
| 152 | m_loadQueue += target; |
| 153 | } |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | if (moreToLoad) |
| 158 | { |
| 159 | QCoreApplication::postEvent(this, new NextThumbnailEvent(this)); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | void ThumbnailCache::Clear(uint64_t frame, int seekamount) |
| 164 | { |
| 165 | QMutexLocker l(&m_cacheLock); |
| 166 | for (int i = 0 ; i < m_images.size(); ++i) |
| 167 | m_images[i].m_image->Reset(); |
| 168 | m_displayed.clear(); |
| 169 | m_baseFrame = frame; |
| 170 | m_seekamount = seekamount; |
| 171 | while (m_totalDataBytes > m_maxDataBytes && !m_cache.isEmpty()) |
| 172 | { |
| 173 | // find and delete the oldest image |
| 174 | uint64_t oldestIndex = -1; |
| 175 | uint64_t oldestTimestamp = m_timestamp; |
| 176 | QHash<uint64_t, ThumbnailCacheEntry>::iterator iter = m_cache.begin(); |
| 177 | while (m_totalDataBytes > m_maxDataBytes && iter != m_cache.end()) |
| 178 | { |
| 179 | uint64_t key = iter.key(); |
| 180 | ++iter; |
| 181 | uint64_t timestamp = m_cache[key].m_timestamp; |
| 182 | if (timestamp < oldestTimestamp) |
| 183 | { |
| 184 | oldestIndex = key; |
| 185 | oldestTimestamp = timestamp; |
| 186 | } |
| 187 | } |
| 188 | if (oldestIndex == (uint64_t)-1) // this shouldn't happen |
| 189 | break; |
| 190 | uint64_t key = oldestIndex; |
| 191 | ThumbnailCacheEntry entry = m_cache.take(key); |
| 192 | delete entry.m_image; |
| 193 | delete entry.m_data; |
| 194 | m_totalDataBytes -= entry.m_dataLength; |
| 195 | LOG(VB_PLAYBACK, LOG_DEBUG, |
| 196 | QString("Expired thumbnail frame=%1 size=%2 new cache size=%3") |
| 197 | .arg(key).arg(entry.m_dataLength).arg(m_totalDataBytes)); |
| 198 | } |
| 199 | m_loadQueue.clear(); |
| 200 | } |
| 201 | |
| 202 | //////////////////////////////////////////////////////////////// |
| 203 | |
| 385 | |
| 386 | // First-time creation of thumbnail loader/cacher |
| 387 | if (!m_thumbnails) |
| 388 | { |
| 389 | MythScreenType *win = osd->GetWindow("osd_program_editor"); |
| 390 | QVector<ThumbnailLayout> images; |
| 391 | if (win) |
| 392 | { |
| 393 | QList<MythUIType *> *children = win->GetAllChildren(); |
| 394 | if (children) |
| 395 | { |
| 396 | QList<MythUIType *>::const_iterator iter = |
| 397 | children->constBegin(); |
| 398 | for (; iter != children->constEnd(); ++iter) |
| 399 | { |
| 400 | static const char *prefix = "thumbnail_"; |
| 401 | QString name = (*iter)->objectName(); |
| 402 | if (!name.startsWith(prefix)) |
| 403 | continue; |
| 404 | MythUIImage *imageType = dynamic_cast<MythUIImage*> (*iter); |
| 405 | if (!imageType) |
| 406 | continue; |
| 407 | bool ok = false; |
| 408 | float steps = name.mid(strlen(prefix)).toFloat(&ok); |
| 409 | if (!ok) |
| 410 | continue; |
| 411 | images += |
| 412 | ThumbnailLayout(imageType, steps, |
| 413 | imageType->GetArea().toQRect().size()); |
| 414 | } |
| 415 | } |
| 416 | } |
| 417 | |
| 418 | m_thumbnails = new ThumbnailCache(m_ctx, images); |
| 419 | // Start helper thread only if the theme contains thumbnail |
| 420 | // objects. |
| 421 | if (!images.isEmpty()) |
| 422 | m_thumbnails->start(); |
| 423 | } |
| 424 | |
| 425 | m_thumbnails->Clear(m_ctx->player->GetFramesPlayed(), m_seekamount); |
| 426 | m_thumbnails->Load(); |
| 427 | } |
| 428 | |
| 429 | DeleteMap::~DeleteMap(void) |
| 430 | { |
| 431 | if (m_thumbnails) |
| 432 | delete m_thumbnails; |