Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "media_player.h"
19 : #include "client/videomanager.h"
20 : #include "client/jami_signal.h"
21 : #include "jami/media_const.h"
22 : #include <string>
23 : namespace jami {
24 :
25 : static constexpr auto MS_PER_PACKET = std::chrono::milliseconds(20);
26 :
27 11 : MediaPlayer::MediaPlayer(const std::string& resource)
28 11 : : loop_(std::bind(&MediaPlayer::configureMediaInputs, this), std::bind(&MediaPlayer::process, this), [] {})
29 : {
30 11 : auto suffix = resource;
31 : static const std::string& sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
32 11 : const auto pos = resource.find(sep);
33 11 : if (pos != std::string::npos) {
34 5 : suffix = resource.substr(pos + sep.size());
35 : }
36 :
37 11 : path_ = suffix;
38 :
39 11 : audioInput_ = jami::getAudioInput(path_);
40 11 : audioInput_->setPaused(paused_);
41 : #ifdef ENABLE_VIDEO
42 11 : videoInput_ = jami::getVideoInput(path_, video::VideoInputMode::ManagedByDaemon, resource);
43 11 : videoInput_->setPaused(paused_);
44 : #endif
45 :
46 11 : demuxer_ = std::make_shared<MediaDemuxer>();
47 11 : loop_.start();
48 11 : }
49 :
50 11 : MediaPlayer::~MediaPlayer()
51 : {
52 11 : pause(true);
53 11 : loop_.join();
54 11 : audioInput_.reset();
55 : #ifdef ENABLE_VIDEO
56 11 : videoInput_.reset();
57 : #endif
58 11 : }
59 :
60 : bool
61 11 : MediaPlayer::configureMediaInputs()
62 : {
63 11 : DeviceParams devOpts = {};
64 11 : devOpts.input = path_;
65 11 : devOpts.name = path_;
66 11 : devOpts.loop = "1";
67 :
68 11 : size_t dot = path_.find_last_of('.');
69 11 : std::string ext = dot == std::string::npos ? "" : path_.substr(dot + 1);
70 11 : bool decodeImg = (ext == "jpeg" || ext == "jpg" || ext == "png" || ext == "pdf");
71 :
72 : // Force 1fps for static image
73 11 : if (decodeImg) {
74 1 : devOpts.format = "image2";
75 1 : devOpts.framerate = 1;
76 : } else {
77 10 : JAMI_WARNING("Guessing file type for {}", path_);
78 : }
79 :
80 11 : if (demuxer_->openInput(devOpts) < 0) {
81 0 : emitInfo();
82 0 : return false;
83 : }
84 11 : demuxer_->findStreamInfo();
85 :
86 11 : pauseInterval_ = 0;
87 11 : startTime_ = av_gettime();
88 11 : lastPausedTime_ = startTime_;
89 :
90 : try {
91 11 : audioStream_ = demuxer_->selectStream(AVMEDIA_TYPE_AUDIO);
92 11 : if (hasAudio()) {
93 10 : audioInput_->configureFilePlayback(path_, demuxer_, audioStream_);
94 10 : audioInput_->updateStartTime(startTime_);
95 10 : audioInput_->start();
96 : }
97 0 : } catch (const std::exception& e) {
98 0 : JAMI_ERROR("MediaPlayer {} open audio input failed: {}", path_, e.what());
99 0 : }
100 : #ifdef ENABLE_VIDEO
101 : try {
102 11 : videoStream_ = demuxer_->selectStream(AVMEDIA_TYPE_VIDEO);
103 11 : if (hasVideo()) {
104 10 : videoInput_->configureFilePlayback(path_, demuxer_, videoStream_);
105 10 : videoInput_->updateStartTime(startTime_);
106 : }
107 0 : } catch (const std::exception& e) {
108 0 : videoInput_ = nullptr;
109 0 : JAMI_ERROR("MediaPlayer {} open video input failed: {}", path_, e.what());
110 0 : }
111 : #endif
112 :
113 11 : demuxer_->setNeedFrameCb([this]() -> void { readBufferOverflow_ = false; });
114 :
115 11 : demuxer_->setFileFinishedCb([this](bool isAudio) -> void {
116 0 : if (isAudio) {
117 0 : audioStreamEnded_ = true;
118 : } else {
119 0 : videoStreamEnded_ = true;
120 : }
121 0 : });
122 :
123 11 : if (decodeImg) {
124 1 : fileDuration_ = 0;
125 : } else {
126 10 : fileDuration_ = demuxer_->getDuration();
127 10 : if (fileDuration_ <= 0) {
128 0 : emitInfo();
129 0 : return false;
130 : }
131 : }
132 :
133 11 : emitInfo();
134 11 : demuxer_->updateCurrentState(MediaDemuxer::CurrentState::Demuxing);
135 11 : return true;
136 11 : }
137 :
138 : void
139 91043097 : MediaPlayer::process()
140 : {
141 91043097 : if (!demuxer_)
142 0 : return;
143 91043097 : if (fileDuration_ > 0 && streamsFinished()) {
144 0 : audioStreamEnded_ = false;
145 0 : videoStreamEnded_ = false;
146 0 : playFileFromBeginning();
147 : }
148 :
149 91043097 : if (paused_ || readBufferOverflow_) {
150 11 : std::this_thread::sleep_for(MS_PER_PACKET);
151 11 : return;
152 : }
153 :
154 91043086 : const auto ret = demuxer_->demuxe();
155 91043086 : switch (ret) {
156 755 : case MediaDemuxer::Status::Success:
157 : case MediaDemuxer::Status::FallBack:
158 755 : break;
159 91042331 : case MediaDemuxer::Status::EndOfFile:
160 91042331 : demuxer_->updateCurrentState(MediaDemuxer::CurrentState::Finished);
161 91042331 : break;
162 0 : case MediaDemuxer::Status::ReadError:
163 0 : JAMI_ERROR("Failed to decode frame");
164 0 : break;
165 0 : case MediaDemuxer::Status::ReadBufferOverflow:
166 0 : readBufferOverflow_ = true;
167 0 : break;
168 0 : case MediaDemuxer::Status::RestartRequired:
169 : default:
170 0 : break;
171 : }
172 : }
173 :
174 : void
175 11 : MediaPlayer::emitInfo()
176 : {
177 0 : std::map<std::string, std::string> info {{"duration", std::to_string(fileDuration_)},
178 11 : {"audio_stream", std::to_string(audioStream_)},
179 55 : {"video_stream", std::to_string(videoStream_)}};
180 11 : emitSignal<libjami::MediaPlayerSignal::FileOpened>(path_, info);
181 22 : }
182 :
183 : bool
184 3 : MediaPlayer::isInputValid()
185 : {
186 3 : return !path_.empty();
187 : }
188 :
189 : void
190 0 : MediaPlayer::muteAudio(bool mute)
191 : {
192 0 : if (hasAudio()) {
193 0 : audioInput_->setMuted(mute);
194 : }
195 0 : }
196 :
197 : void
198 20 : MediaPlayer::pause(bool pause)
199 : {
200 20 : if (pause == paused_) {
201 4 : return;
202 : }
203 16 : paused_ = pause;
204 16 : if (!pause) {
205 8 : pauseInterval_ += av_gettime() - lastPausedTime_;
206 : } else {
207 8 : lastPausedTime_ = av_gettime();
208 : }
209 16 : auto newTime = startTime_ + pauseInterval_;
210 16 : if (hasAudio()) {
211 11 : audioInput_->setPaused(paused_);
212 11 : audioInput_->updateStartTime(newTime);
213 : }
214 : #ifdef ENABLE_VIDEO
215 16 : if (hasVideo()) {
216 11 : videoInput_->setPaused(paused_);
217 11 : videoInput_->updateStartTime(newTime);
218 : }
219 : #endif
220 : }
221 :
222 : bool
223 7 : MediaPlayer::seekToTime(int64_t time)
224 : {
225 7 : if (time < 0 || time > fileDuration_) {
226 1 : return false;
227 : }
228 6 : if (time >= fileDuration_) {
229 0 : playFileFromBeginning();
230 0 : return true;
231 : }
232 :
233 6 : bool wasPaused = paused_;
234 6 : if (!wasPaused)
235 1 : pause(true);
236 :
237 6 : if (!demuxer_->seekFrame(-1, time)) {
238 0 : if (!wasPaused)
239 0 : pause(false);
240 0 : return false;
241 : }
242 6 : flushMediaBuffers();
243 6 : demuxer_->updateCurrentState(MediaDemuxer::CurrentState::Demuxing);
244 :
245 6 : int64_t currentTime = av_gettime();
246 6 : if (wasPaused) {
247 5 : pauseInterval_ += currentTime - lastPausedTime_;
248 5 : lastPausedTime_ = currentTime;
249 : }
250 :
251 6 : startTime_ = currentTime - pauseInterval_ - time;
252 6 : if (hasAudio()) {
253 6 : audioInput_->setSeekTime(time);
254 6 : audioInput_->updateStartTime(startTime_);
255 : }
256 : #ifdef ENABLE_VIDEO
257 6 : if (hasVideo()) {
258 6 : videoInput_->setSeekTime(time);
259 6 : videoInput_->updateStartTime(startTime_);
260 : }
261 : #endif
262 :
263 6 : if (!wasPaused)
264 1 : pause(false);
265 6 : return true;
266 : }
267 : void
268 0 : MediaPlayer::playFileFromBeginning()
269 : {
270 0 : pause(true);
271 0 : demuxer_->updateCurrentState(MediaDemuxer::CurrentState::Demuxing);
272 0 : if (!demuxer_->seekFrame(-1, 0)) {
273 0 : return;
274 : }
275 0 : flushMediaBuffers();
276 0 : startTime_ = av_gettime();
277 0 : lastPausedTime_ = startTime_;
278 0 : pauseInterval_ = 0;
279 0 : if (hasAudio()) {
280 0 : audioInput_->updateStartTime(startTime_);
281 : }
282 : #ifdef ENABLE_VIDEO
283 0 : if (hasVideo()) {
284 0 : videoInput_->updateStartTime(startTime_);
285 : }
286 : #endif
287 0 : if (autoRestart_)
288 0 : pause(false);
289 : }
290 :
291 : void
292 6 : MediaPlayer::flushMediaBuffers()
293 : {
294 : #ifdef ENABLE_VIDEO
295 6 : if (hasVideo()) {
296 6 : videoInput_->flushBuffers();
297 : }
298 : #endif
299 :
300 6 : if (hasAudio()) {
301 6 : audioInput_->flushBuffers();
302 : }
303 6 : }
304 :
305 : const std::string&
306 3 : MediaPlayer::getId() const
307 : {
308 3 : return path_;
309 : }
310 :
311 : int64_t
312 11 : MediaPlayer::getPlayerPosition() const
313 : {
314 11 : if (paused_) {
315 9 : return lastPausedTime_ - startTime_ - pauseInterval_;
316 : }
317 2 : return av_gettime() - startTime_ - pauseInterval_;
318 : }
319 :
320 : int64_t
321 0 : MediaPlayer::getPlayerDuration() const
322 : {
323 0 : return fileDuration_;
324 : }
325 :
326 : bool
327 5 : MediaPlayer::isPaused() const
328 : {
329 5 : return paused_;
330 : }
331 :
332 : bool
333 91043096 : MediaPlayer::streamsFinished()
334 : {
335 91043096 : bool audioFinished = hasAudio() ? audioStreamEnded_ : true;
336 91043096 : bool videoFinished = hasVideo() ? videoStreamEnded_ : true;
337 91043096 : return audioFinished && videoFinished;
338 : }
339 :
340 : namespace {
341 :
342 : using PreviewClock = std::chrono::steady_clock;
343 :
344 : constexpr auto PREVIEW_TIMEOUT = std::chrono::seconds(3);
345 :
346 : int
347 0 : deadlineReached(void* opaque)
348 : {
349 0 : return PreviewClock::now() >= *static_cast<const PreviewClock::time_point*>(opaque);
350 : }
351 :
352 : #ifdef ENABLE_VIDEO
353 : std::shared_ptr<MediaFrame>
354 0 : decodeFirstFrame(const std::shared_ptr<MediaDemuxer>& demuxer, int stream, PreviewClock::time_point deadline)
355 : {
356 0 : std::shared_ptr<MediaFrame> first;
357 0 : MediaDecoder decoder(demuxer, stream, [&first](std::shared_ptr<MediaFrame>&& frame) {
358 0 : if (!first)
359 0 : first = std::move(frame);
360 0 : });
361 0 : bool rewound = false;
362 0 : while (decoder.isReady() && !first && PreviewClock::now() < deadline) {
363 0 : const auto status = demuxer->decode();
364 0 : if (status == MediaDemuxer::Status::FallBack) {
365 0 : if (rewound || !demuxer->seekFrame(stream, 0))
366 0 : break;
367 0 : rewound = true;
368 0 : continue;
369 : }
370 0 : if (status == MediaDemuxer::Status::EndOfFile || status == MediaDemuxer::Status::ReadError
371 0 : || status == MediaDemuxer::Status::RestartRequired)
372 : break;
373 : }
374 0 : if (decoder.isReady() && !first && PreviewClock::now() < deadline)
375 0 : decoder.flush();
376 0 : demuxer->setStreamCallback(stream);
377 0 : return first;
378 0 : }
379 : #endif
380 :
381 : } // namespace
382 :
383 : libjami::MediaPreview
384 0 : getMediaPreview(const std::string& path)
385 : {
386 0 : libjami::MediaPreview preview;
387 0 : preview.info = {{"duration", "0"}, {"audio_stream", "-1"}, {"video_stream", "-1"}};
388 : // Must outlive the demuxer, which reads it from FFmpeg's interrupt callback.
389 0 : auto deadline = PreviewClock::now() + PREVIEW_TIMEOUT;
390 0 : auto demuxer = std::make_shared<MediaDemuxer>();
391 0 : demuxer->setInterruptCallback(deadlineReached, &deadline);
392 0 : DeviceParams params;
393 0 : params.input = path;
394 0 : params.name = path;
395 0 : if (demuxer->openInput(params) < 0)
396 0 : return preview;
397 0 : demuxer->findStreamInfo();
398 :
399 0 : const auto audioStream = std::max(-1, demuxer->selectStream(AVMEDIA_TYPE_AUDIO));
400 0 : const auto videoStream = std::max(-1, demuxer->selectStream(AVMEDIA_TYPE_VIDEO));
401 0 : preview.info["duration"] = std::to_string(std::max<int64_t>(0, demuxer->getDuration()));
402 0 : preview.info["audio_stream"] = std::to_string(audioStream);
403 0 : preview.info["video_stream"] = std::to_string(videoStream);
404 :
405 : #ifdef ENABLE_VIDEO
406 0 : if (videoStream >= 0) {
407 0 : if (auto frame = decodeFirstFrame(demuxer, videoStream, deadline)) {
408 0 : preview.frame.reset(av_frame_clone(frame->pointer()));
409 0 : }
410 : }
411 : #endif
412 0 : return preview;
413 0 : }
414 :
415 : } // namespace jami
|