blob: 4c1881a3f9927dd27fd68a5d0196f528de13f2ff [file] [log] [blame]
agsantos4bb4bc52021-03-08 14:21:45 -05001/**
2 * Copyright (C) 2021 Savoir-faire Linux Inc.
3 *
4 * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21#include "WatermarkVideoSubscriber.h"
22
23extern "C" {
24#include <libavutil/display.h>
25}
26#include <accel.h>
27#include <frameScaler.h>
Aline Gondim Santos4b62db12022-10-06 18:20:07 -030028#include <common.h>
agsantos4bb4bc52021-03-08 14:21:45 -050029
30#include <pluglog.h>
31#include <algorithm>
32#include <ctime>
33#include <clocale>
34#include <iostream>
35
36const std::string TAG = "Watermark";
37const char sep = separator();
38
39namespace jami {
40
agsantos3fcc6212021-08-18 17:11:29 -030041WatermarkVideoSubscriber::WatermarkVideoSubscriber(const std::string& dataPath)
agsantos4bb4bc52021-03-08 14:21:45 -050042{
43 if (std::setlocale(LC_TIME, std::locale("").name().c_str()) == NULL) {
44 Plog::log(Plog::LogPriority::INFO, TAG, "error while setting locale");
45 }
agsantos27e01fd2021-11-08 11:17:37 -050046
47 std::setlocale(LC_NUMERIC, "C");
Aline Gondim Santos4b62db12022-10-06 18:20:07 -030048 fontFile_ = string_utils::ffmpegFormatString(dataPath + sep + "Muli-Light.ttf");
agsantos4bb4bc52021-03-08 14:21:45 -050049}
50
51WatermarkVideoSubscriber::~WatermarkVideoSubscriber()
52{
53 validLogo_ = false;
54 logoFilter_.clean();
55 detach();
56 std::lock_guard<std::mutex> lk(mtx_);
57 Plog::log(Plog::LogPriority::INFO, TAG, "~WatermarkMediaProcessor");
58}
59
60MediaStream
61WatermarkVideoSubscriber::getLogoAVFrameInfos()
62{
63 AVFormatContext* ctx = avformat_alloc_context();
64
65 // Open
66 if (avformat_open_input(&ctx, logoPath_.c_str(), NULL, NULL) != 0) {
67 avformat_free_context(ctx);
68 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't open input stream.");
Aline Gondim Santosbd75c212022-10-03 12:28:26 -030069 validLogo_ = false;
agsantos4bb4bc52021-03-08 14:21:45 -050070 return {};
71 }
72 pFormatCtx_.reset(ctx);
73 // Retrieve stream information
74 if (avformat_find_stream_info(pFormatCtx_.get(), NULL) < 0) {
75 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't find stream information.");
Aline Gondim Santosbd75c212022-10-03 12:28:26 -030076 validLogo_ = false;
agsantos4bb4bc52021-03-08 14:21:45 -050077 return {};
78 }
79
80 // Dump valid information onto standard error
81 av_dump_format(pFormatCtx_.get(), 0, logoPath_.c_str(), false);
82
83 // Find the video stream
84 for (int i = 0; i < static_cast<int>(pFormatCtx_->nb_streams); i++)
85 if (pFormatCtx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
86 videoStream_ = i;
87 break;
88 }
89
90 if (videoStream_ == -1) {
91 Plog::log(Plog::LogPriority::INFO, TAG, "Didn't find a video stream.");
Aline Gondim Santosbd75c212022-10-03 12:28:26 -030092 validLogo_ = false;
agsantos4bb4bc52021-03-08 14:21:45 -050093 return {};
94 }
95
96 rational<int> fr = pFormatCtx_->streams[videoStream_]->r_frame_rate;
97 return MediaStream("logo",
98 pFormatCtx_->streams[videoStream_]->codecpar->format,
99 1 / fr,
100 pFormatCtx_->streams[videoStream_]->codecpar->width,
101 pFormatCtx_->streams[videoStream_]->codecpar->height,
102 0,
103 fr);
104}
105
106void
107WatermarkVideoSubscriber::loadMarkLogo()
108{
109 if (logoPath_.empty())
110 return;
111
112 logoFilter_.clean();
113 logoDescription_ = "[logo]scale=" + logoSize_ + "*" + std::to_string(pluginFrameSize_.first)
114 + ":" + logoSize_ + "*" + std::to_string(pluginFrameSize_.second)
115 + ":force_original_aspect_ratio='decrease',format=yuva444p,"
116 "split=2[bg][fg],[bg]drawbox=c='"
117 + backgroundColor_
118 + "':replace=1:t=fill[bg],"
119 "[bg][fg]overlay=format=auto";
120 Plog::log(Plog::LogPriority::INFO, TAG, logoDescription_);
121 logoStream_ = getLogoAVFrameInfos();
122 logoFilter_.initialize(logoDescription_, {logoStream_});
123
agsantos4bb4bc52021-03-08 14:21:45 -0500124 AVCodecContext* pCodecCtx;
agsantos4bb4bc52021-03-08 14:21:45 -0500125
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300126 const AVCodec* pCodec = avcodec_find_decoder(
127 pFormatCtx_->streams[videoStream_]->codecpar->codec_id);
agsantos4bb4bc52021-03-08 14:21:45 -0500128 if (pCodec == nullptr) {
129 pFormatCtx_.reset();
130 Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
131 validLogo_ = false;
132 return;
133 }
134
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300135 pCodecCtx = avcodec_alloc_context3(pCodec);
agsantos4bb4bc52021-03-08 14:21:45 -0500136 // Open codec
137 if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
138 pFormatCtx_.reset();
139 Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
140 validLogo_ = false;
141 return;
142 }
143
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300144 AVPacket* packet = av_packet_alloc();
145
agsantos4bb4bc52021-03-08 14:21:45 -0500146 if (av_read_frame(pFormatCtx_.get(), packet) < 0) {
147 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300148 avcodec_free_context(&pCodecCtx);
149 av_packet_unref(packet);
agsantos4bb4bc52021-03-08 14:21:45 -0500150 av_packet_free(&packet);
151 pFormatCtx_.reset();
152 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from context.");
153 validLogo_ = false;
154 return;
155 }
156
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300157 if (avcodec_send_packet(pCodecCtx, packet) < 0) {
158 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300159 avcodec_free_context(&pCodecCtx);
160 av_packet_unref(packet);
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300161 av_packet_free(&packet);
162 pFormatCtx_.reset();
163 Plog::log(Plog::LogPriority::INFO, TAG, "Could not send packet no codec.");
164 validLogo_ = false;
165 return;
agsantos4bb4bc52021-03-08 14:21:45 -0500166 }
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300167
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300168 uniqueFramePtr logoImage = {av_frame_alloc(), frameFree};
169 if (avcodec_receive_frame(pCodecCtx, logoImage.get()) < 0) {
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300170 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300171 avcodec_free_context(&pCodecCtx);
172 av_packet_unref(packet);
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300173 av_packet_free(&packet);
174 pFormatCtx_.reset();
175 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from codec.");
176 validLogo_ = false;
177 return;
178 }
179
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300180 logoFilter_.feedInput(logoImage.get(), "logo");
agsantos4bb4bc52021-03-08 14:21:45 -0500181 logoFilter_.feedEOF("logo");
182
agsantos4bb4bc52021-03-08 14:21:45 -0500183 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300184 avcodec_free_context(&pCodecCtx);
185 av_packet_unref(packet);
agsantos4bb4bc52021-03-08 14:21:45 -0500186 av_packet_free(&packet);
187 pFormatCtx_.reset();
188 mark_.reset(logoFilter_.readOutput());
189 mark_->pts = 0;
190 mark_->best_effort_timestamp = 0;
191 validLogo_ = mark_->width && mark_->height;
192}
193
194void
195WatermarkVideoSubscriber::setParameter(std::string& parameter, Parameter type)
196{
197 switch (type) {
198 case (Parameter::LOGOSIZE):
199 logoSize_ = parameter;
200 break;
201 case (Parameter::TIMEZONE):
202 timeZone_ = parameter == "1";
203 return;
204 case (Parameter::FONTSIZE):
205 fontSize_ = std::stoi(parameter);
206 break;
207 case (Parameter::LOGOBACKGROUND):
208 backgroundColor_ = parameter;
209 if (backgroundColor_.find("black") == std::string::npos) {
210 fontColor_ = "black";
211 fontBackground_ = "white@0.5";
212 } else {
213 fontColor_ = "white";
214 fontBackground_ = "black@0.5";
215 }
agsantos4bb4bc52021-03-08 14:21:45 -0500216 break;
217 case (Parameter::SHOWLOGO):
218 showLogo_ = parameter == "1";
219 break;
220 case (Parameter::SHOWINFOS):
221 showInfos_ = parameter == "1";
222 break;
223 case (Parameter::LOGOPATH):
224 logoPath_ = parameter;
225 break;
226 case (Parameter::TIME):
227 time_ = parameter == "1";
228 break;
229 case (Parameter::DATE):
230 date_ = parameter == "1";
231 break;
232 case (Parameter::TIMEFORMAT):
233 timeFormat_ = "%{localtime\\:'" + parameter + "'}";
234 if (timeZone_)
235 timeFormat_ = "%{localtime\\:'" + parameter + " %Z'}";
236 break;
237 case (Parameter::DATEFORMAT):
238 dateFormat_ = "%{localtime\\:'" + parameter + "'}";
239 break;
240 case (Parameter::LOCATION):
241 location_ = parameter;
242 break;
243 case (Parameter::INFOSPOSITION):
244 infosposition_ = parameter;
245 break;
246 case (Parameter::LOGOPOSITION):
247 logoposition_ = parameter;
248 break;
249 default:
250 return;
251 }
252
253 firstRun = true;
254}
255
256void
257WatermarkVideoSubscriber::setFilterDescription()
258{
259 loadMarkLogo();
260
261 std::string infoSep = ", ";
262 if (pluginFrameSize_.first < pluginFrameSize_.second)
263 infoSep = ",\n";
264
265 std::vector<std::string> infos;
266 infosSize_ = 0;
267 if (!location_.empty()) {
268 infosSize_++;
269 infos.emplace_back(location_);
270 }
271 if (date_) {
272 infosSize_++;
273 infos.emplace_back(dateFormat_);
274 }
275 if (time_) {
276 infosSize_++;
277 infos.emplace_back(timeFormat_);
278 }
279 infosString.clear();
280 for (int i = 0; i < infosSize_ - 1; i++)
281 infosString += infos[i] + infoSep;
282 if (infosSize_ > 0)
283 infosString += infos.back();
284
285 setMarkPosition();
286
287 std::string rotateSides = "";
288 if (std::abs(angle_) == 90)
289 rotateSides = ":out_w=ih:out_h=iw";
Aline Gondim Santos4b62db12022-10-06 18:20:07 -0300290 std::string formatedPath = string_utils::ffmpegFormatString(logoPath_);
agsantos4bb4bc52021-03-08 14:21:45 -0500291
Aline Gondim Santos4b62db12022-10-06 18:20:07 -0300292 auto gifDescription = "movie='" + formatedPath + "':loop=0,setpts=N/(FR*TB)[logo],"
293 + logoDescription_ + "[loop],";
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300294
agsantos4bb4bc52021-03-08 14:21:45 -0500295 if (angle_ != 0)
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300296 pluginFilterDescription_ = gifDescription
297 + "[input]rotate=" + rotation[angle_] + rotateSides
298 + "[rot],[rot][loop]overlay=" + std::to_string(points_[0].first)
agsantos4bb4bc52021-03-08 14:21:45 -0500299 + ":" + std::to_string(points_[0].second)
300 + ",rotate=" + rotation[-angle_] + rotateSides;
301 else
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300302 pluginFilterDescription_ = gifDescription
303 + "[input][loop]overlay="
304 + std::to_string(points_[0].first) + ":"
agsantos4bb4bc52021-03-08 14:21:45 -0500305 + std::to_string(points_[0].second);
306
307 std::string baseInfosDescription = "[input]rotate=" + rotation[angle_] + rotateSides
308 + ",drawtext=fontfile='" + fontFile_ + "':text='"
309 + infosString + "':fontcolor=" + fontColor_
310 + ":fontsize=" + std::to_string(fontSize_)
311 + ":line_spacing=" + std::to_string(lineSpacing_)
312 + ":box=1:boxcolor=" + fontBackground_ + ":boxborderw=5:x=";
313
314 if (infosposition_ == "1")
315 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
316 + "-text_w:y=" + std::to_string(points_[1].second);
317 else if (infosposition_ == "2")
318 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
319 + ":y=" + std::to_string(points_[1].second);
320 else if (infosposition_ == "3")
321 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
322 + ":y=" + std::to_string(points_[1].second) + "-text_h";
323 else if (infosposition_ == "4")
324 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
325 + "-text_w:y=" + std::to_string(points_[1].second) + "-text_h";
agsantos1bbc7cc2021-05-20 16:43:35 -0400326 infosDescription_ += ",rotate=" + rotation[-angle_] + rotateSides + ",format=yuv420p";
agsantos4bb4bc52021-03-08 14:21:45 -0500327
328 Plog::log(Plog::LogPriority::INFO, TAG, infosDescription_);
329 Plog::log(Plog::LogPriority::INFO, TAG, pluginFilterDescription_);
330}
331
332void
333WatermarkVideoSubscriber::setMarkPosition()
334{
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300335 if (!validLogo_)
336 return;
agsantos4bb4bc52021-03-08 14:21:45 -0500337 // 1, 2, 3, and 4 are cartesian positions
338 int margin = 10;
339 int markWidth = showLogo_ ? mark_->width : 0;
340 int markHeight = showLogo_ ? mark_->height : 0;
341 int infoHeight = (std::abs(angle_) == 90) ? (fontSize_ + lineSpacing_) * infosSize_
342 : lineSpacing_ * 2 + fontSize_;
343 if (pluginFrameSize_.first == 0 || pluginFrameSize_.second == 0)
344 return;
345
346 if (infosposition_ == "1") {
347 points_[1] = {pluginFrameSize_.first - margin, margin};
348 } else if (infosposition_ == "2") {
349 points_[1] = {margin, margin};
350 } else if (infosposition_ == "3") {
351 points_[1] = {margin, pluginFrameSize_.second - margin};
352 } else if (infosposition_ == "4") {
353 points_[1] = {pluginFrameSize_.first - margin, pluginFrameSize_.second - margin};
354 }
355 if (logoposition_ == "1") {
356 points_[0] = {pluginFrameSize_.first - mark_->width - margin / 2, margin};
357 } else if (logoposition_ == "2") {
358 points_[0] = {margin / 2, margin};
359 } else if (logoposition_ == "3") {
360 points_[0] = {margin / 2, pluginFrameSize_.second - markHeight - margin};
361 } else if (logoposition_ == "4") {
362 points_[0] = {pluginFrameSize_.first - markWidth - margin / 2,
363 pluginFrameSize_.second - markHeight - margin};
364 }
365
366 if (infosposition_ == logoposition_ && showInfos_ && showLogo_) {
367 if (logoposition_ == "1" || logoposition_ == "2") {
368 points_[0].second += infoHeight;
369 } else if (logoposition_ == "3" || logoposition_ == "4") {
370 points_[0].second -= infoHeight;
371 }
372 }
373}
374
375void
376WatermarkVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& pluginFrame)
377{
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300378 if (!observable_ || !pluginFrame || (showLogo_ && !validLogo_))
agsantos4bb4bc52021-03-08 14:21:45 -0500379 return;
380
381 AVFrameSideData* side_data = av_frame_get_side_data(pluginFrame, AV_FRAME_DATA_DISPLAYMATRIX);
382 int newAngle {0};
383 if (side_data) {
384 auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
385 newAngle = static_cast<int>(av_display_rotation_get(matrix_rotation));
386 }
387 if (newAngle != angle_) {
388 angle_ = newAngle;
389 firstRun = true;
390 }
391
392 //======================================================================================
393 // GET RAW FRAME
agsantos1bbc7cc2021-05-20 16:43:35 -0400394 uniqueFramePtr rgbFrame = {transferToMainMemory(pluginFrame, AV_PIX_FMT_NV12), frameFree};
395 rgbFrame.reset(FrameScaler::convertFormat(rgbFrame.get(), AV_PIX_FMT_YUV420P));
agsantos4bb4bc52021-03-08 14:21:45 -0500396 if (!rgbFrame.get())
397 return;
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300398
399 if (sourceTimeBase_.num != pluginFrame->time_base.num || sourceTimeBase_.den != pluginFrame->time_base.den)
400 firstRun = true;
401
402 rgbFrame->pts = pluginFrame->pts;
403 rgbFrame->time_base = pluginFrame->time_base;
404 sourceTimeBase_ = pluginFrame->time_base;
agsantos4bb4bc52021-03-08 14:21:45 -0500405
406 if (firstRun) {
407 pluginFilter_.clean();
408 infosFilter_.clean();
409 pluginFrameSize_ = {rgbFrame->width, rgbFrame->height};
410 if (std::abs(angle_) == 90)
411 pluginFrameSize_ = {rgbFrame->height, rgbFrame->width};
412
413 setFilterDescription();
414
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300415 rational<int> fr(sourceTimeBase_.den, sourceTimeBase_.num);
agsantos4bb4bc52021-03-08 14:21:45 -0500416 pluginstream_ = MediaStream("input",
417 rgbFrame->format,
418 1 / fr,
419 rgbFrame->width,
420 rgbFrame->height,
421 0,
422 fr);
423
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300424 if (showLogo_ && validLogo_) {
425 pluginFilter_.initialize(pluginFilterDescription_, {pluginstream_});
agsantos4bb4bc52021-03-08 14:21:45 -0500426 }
427
428 infosFilter_.initialize(infosDescription_, {pluginstream_});
429 firstRun = false;
430 }
431
432 if (!infosFilter_.initialized_ && !pluginFilter_.initialized_)
433 return;
434
Aline Gondim Santosbd75c212022-10-03 12:28:26 -0300435 if (showLogo_ && validLogo_) {
agsantos4bb4bc52021-03-08 14:21:45 -0500436 if (pluginFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400437 uniqueFramePtr filteredFrame = {pluginFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500438 if (filteredFrame.get())
439 moveFrom(rgbFrame.get(), filteredFrame.get());
440 }
441 }
442 if (showInfos_) {
443 if (infosFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400444 uniqueFramePtr filteredFrame = {infosFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500445 if (filteredFrame.get())
446 moveFrom(rgbFrame.get(), filteredFrame.get());
447 }
448 }
449 if (showInfos_ || showLogo_) {
450 moveFrom(pluginFrame, rgbFrame.get());
451 }
452}
453
454void
455WatermarkVideoSubscriber::attached(jami::Observable<AVFrame*>* observable)
456{
457 Plog::log(Plog::LogPriority::INFO, TAG, "Attached!");
458 observable_ = observable;
459}
460
461void
462WatermarkVideoSubscriber::detached(jami::Observable<AVFrame*>*)
463{
464 pluginFilter_.clean();
465 infosFilter_.clean();
466 firstRun = true;
467 observable_ = nullptr;
468 Plog::log(Plog::LogPriority::INFO, TAG, "Detached!");
469 mtx_.unlock();
470}
471
472void
473WatermarkVideoSubscriber::detach()
474{
475 if (observable_) {
476 mtx_.lock();
477 firstRun = true;
478 observable_->detach(this);
479 }
480}
481} // namespace jami