blob: c53d0de92af8c27a90c824633f737478859bd105 [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>
28
29#include <pluglog.h>
30#include <algorithm>
31#include <ctime>
32#include <clocale>
33#include <iostream>
34
35const std::string TAG = "Watermark";
36const char sep = separator();
37
38namespace jami {
39
agsantos3fcc6212021-08-18 17:11:29 -030040WatermarkVideoSubscriber::WatermarkVideoSubscriber(const std::string& dataPath)
agsantos4bb4bc52021-03-08 14:21:45 -050041{
42 if (std::setlocale(LC_TIME, std::locale("").name().c_str()) == NULL) {
43 Plog::log(Plog::LogPriority::INFO, TAG, "error while setting locale");
44 }
agsantos27e01fd2021-11-08 11:17:37 -050045
46 std::setlocale(LC_NUMERIC, "C");
agsantos4bb4bc52021-03-08 14:21:45 -050047 fontFile_ = dataPath + sep + "Muli-Light.ttf";
48#ifdef WIN32
49 for (int i = fontFile_.size(); i > 0; i--)
50 if (fontFile_[i] == '\\')
51 fontFile_.insert(i, "\\");
52 fontFile_.insert(1, "\\");
53#endif
agsantos4bb4bc52021-03-08 14:21:45 -050054}
55
56WatermarkVideoSubscriber::~WatermarkVideoSubscriber()
57{
58 validLogo_ = false;
59 logoFilter_.clean();
60 detach();
61 std::lock_guard<std::mutex> lk(mtx_);
62 Plog::log(Plog::LogPriority::INFO, TAG, "~WatermarkMediaProcessor");
63}
64
65MediaStream
66WatermarkVideoSubscriber::getLogoAVFrameInfos()
67{
68 AVFormatContext* ctx = avformat_alloc_context();
69
70 // Open
71 if (avformat_open_input(&ctx, logoPath_.c_str(), NULL, NULL) != 0) {
72 avformat_free_context(ctx);
73 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't open input stream.");
74 return {};
75 }
76 pFormatCtx_.reset(ctx);
77 // Retrieve stream information
78 if (avformat_find_stream_info(pFormatCtx_.get(), NULL) < 0) {
79 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't find stream information.");
80 return {};
81 }
82
83 // Dump valid information onto standard error
84 av_dump_format(pFormatCtx_.get(), 0, logoPath_.c_str(), false);
85
86 // Find the video stream
87 for (int i = 0; i < static_cast<int>(pFormatCtx_->nb_streams); i++)
88 if (pFormatCtx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
89 videoStream_ = i;
90 break;
91 }
92
93 if (videoStream_ == -1) {
94 Plog::log(Plog::LogPriority::INFO, TAG, "Didn't find a video stream.");
95 return {};
96 }
97
98 rational<int> fr = pFormatCtx_->streams[videoStream_]->r_frame_rate;
99 return MediaStream("logo",
100 pFormatCtx_->streams[videoStream_]->codecpar->format,
101 1 / fr,
102 pFormatCtx_->streams[videoStream_]->codecpar->width,
103 pFormatCtx_->streams[videoStream_]->codecpar->height,
104 0,
105 fr);
106}
107
108void
109WatermarkVideoSubscriber::loadMarkLogo()
110{
111 if (logoPath_.empty())
112 return;
113
114 logoFilter_.clean();
115 logoDescription_ = "[logo]scale=" + logoSize_ + "*" + std::to_string(pluginFrameSize_.first)
116 + ":" + logoSize_ + "*" + std::to_string(pluginFrameSize_.second)
117 + ":force_original_aspect_ratio='decrease',format=yuva444p,"
118 "split=2[bg][fg],[bg]drawbox=c='"
119 + backgroundColor_
120 + "':replace=1:t=fill[bg],"
121 "[bg][fg]overlay=format=auto";
122 Plog::log(Plog::LogPriority::INFO, TAG, logoDescription_);
123 logoStream_ = getLogoAVFrameInfos();
124 logoFilter_.initialize(logoDescription_, {logoStream_});
125
agsantos4bb4bc52021-03-08 14:21:45 -0500126 AVCodecContext* pCodecCtx;
agsantos4bb4bc52021-03-08 14:21:45 -0500127 AVPacket* packet;
128
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300129 const AVCodec* pCodec = avcodec_find_decoder(
130 pFormatCtx_->streams[videoStream_]->codecpar->codec_id);
agsantos4bb4bc52021-03-08 14:21:45 -0500131 if (pCodec == nullptr) {
132 pFormatCtx_.reset();
133 Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
134 validLogo_ = false;
135 return;
136 }
137
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300138 pCodecCtx = avcodec_alloc_context3(pCodec);
agsantos4bb4bc52021-03-08 14:21:45 -0500139 // Open codec
140 if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
141 pFormatCtx_.reset();
142 Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
143 validLogo_ = false;
144 return;
145 }
146
147 packet = av_packet_alloc();
148 if (av_read_frame(pFormatCtx_.get(), packet) < 0) {
149 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300150 avcodec_free_context(&pCodecCtx);
151 av_packet_unref(packet);
agsantos4bb4bc52021-03-08 14:21:45 -0500152 av_packet_free(&packet);
153 pFormatCtx_.reset();
154 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from context.");
155 validLogo_ = false;
156 return;
157 }
158
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300159 if (avcodec_send_packet(pCodecCtx, packet) < 0) {
160 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300161 avcodec_free_context(&pCodecCtx);
162 av_packet_unref(packet);
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300163 av_packet_free(&packet);
164 pFormatCtx_.reset();
165 Plog::log(Plog::LogPriority::INFO, TAG, "Could not send packet no codec.");
166 validLogo_ = false;
167 return;
agsantos4bb4bc52021-03-08 14:21:45 -0500168 }
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300169
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300170 uniqueFramePtr logoImage = {av_frame_alloc(), frameFree};
171 if (avcodec_receive_frame(pCodecCtx, logoImage.get()) < 0) {
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300172 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300173 avcodec_free_context(&pCodecCtx);
174 av_packet_unref(packet);
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300175 av_packet_free(&packet);
176 pFormatCtx_.reset();
177 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from codec.");
178 validLogo_ = false;
179 return;
180 }
181
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300182 logoFilter_.feedInput(logoImage.get(), "logo");
agsantos4bb4bc52021-03-08 14:21:45 -0500183 logoFilter_.feedEOF("logo");
184
agsantos4bb4bc52021-03-08 14:21:45 -0500185 avcodec_close(pCodecCtx);
Aline Gondim Santosf0e761d2022-08-24 16:13:10 -0300186 avcodec_free_context(&pCodecCtx);
187 av_packet_unref(packet);
agsantos4bb4bc52021-03-08 14:21:45 -0500188 av_packet_free(&packet);
189 pFormatCtx_.reset();
190 mark_.reset(logoFilter_.readOutput());
191 mark_->pts = 0;
192 mark_->best_effort_timestamp = 0;
193 validLogo_ = mark_->width && mark_->height;
194}
195
196void
197WatermarkVideoSubscriber::setParameter(std::string& parameter, Parameter type)
198{
199 switch (type) {
200 case (Parameter::LOGOSIZE):
201 logoSize_ = parameter;
202 break;
203 case (Parameter::TIMEZONE):
204 timeZone_ = parameter == "1";
205 return;
206 case (Parameter::FONTSIZE):
207 fontSize_ = std::stoi(parameter);
208 break;
209 case (Parameter::LOGOBACKGROUND):
210 backgroundColor_ = parameter;
211 if (backgroundColor_.find("black") == std::string::npos) {
212 fontColor_ = "black";
213 fontBackground_ = "white@0.5";
214 } else {
215 fontColor_ = "white";
216 fontBackground_ = "black@0.5";
217 }
218 if (!logoPath_.empty())
219 setParameter(logoPath_, Parameter::LOGOPATH);
220 break;
221 case (Parameter::SHOWLOGO):
222 showLogo_ = parameter == "1";
223 break;
224 case (Parameter::SHOWINFOS):
225 showInfos_ = parameter == "1";
226 break;
227 case (Parameter::LOGOPATH):
228 logoPath_ = parameter;
229 break;
230 case (Parameter::TIME):
231 time_ = parameter == "1";
232 break;
233 case (Parameter::DATE):
234 date_ = parameter == "1";
235 break;
236 case (Parameter::TIMEFORMAT):
237 timeFormat_ = "%{localtime\\:'" + parameter + "'}";
238 if (timeZone_)
239 timeFormat_ = "%{localtime\\:'" + parameter + " %Z'}";
240 break;
241 case (Parameter::DATEFORMAT):
242 dateFormat_ = "%{localtime\\:'" + parameter + "'}";
243 break;
244 case (Parameter::LOCATION):
245 location_ = parameter;
246 break;
247 case (Parameter::INFOSPOSITION):
248 infosposition_ = parameter;
249 break;
250 case (Parameter::LOGOPOSITION):
251 logoposition_ = parameter;
252 break;
253 default:
254 return;
255 }
256
257 firstRun = true;
258}
259
260void
261WatermarkVideoSubscriber::setFilterDescription()
262{
263 loadMarkLogo();
264
265 std::string infoSep = ", ";
266 if (pluginFrameSize_.first < pluginFrameSize_.second)
267 infoSep = ",\n";
268
269 std::vector<std::string> infos;
270 infosSize_ = 0;
271 if (!location_.empty()) {
272 infosSize_++;
273 infos.emplace_back(location_);
274 }
275 if (date_) {
276 infosSize_++;
277 infos.emplace_back(dateFormat_);
278 }
279 if (time_) {
280 infosSize_++;
281 infos.emplace_back(timeFormat_);
282 }
283 infosString.clear();
284 for (int i = 0; i < infosSize_ - 1; i++)
285 infosString += infos[i] + infoSep;
286 if (infosSize_ > 0)
287 infosString += infos.back();
288
289 setMarkPosition();
290
291 std::string rotateSides = "";
292 if (std::abs(angle_) == 90)
293 rotateSides = ":out_w=ih:out_h=iw";
294
295 if (angle_ != 0)
296 pluginFilterDescription_ = "[input]rotate=" + rotation[angle_] + rotateSides
297 + "[rot],[rot][mark]overlay=" + std::to_string(points_[0].first)
298 + ":" + std::to_string(points_[0].second)
299 + ",rotate=" + rotation[-angle_] + rotateSides;
300 else
301 pluginFilterDescription_ = "[input][mark]overlay=" + std::to_string(points_[0].first) + ":"
302 + std::to_string(points_[0].second);
303
304 std::string baseInfosDescription = "[input]rotate=" + rotation[angle_] + rotateSides
305 + ",drawtext=fontfile='" + fontFile_ + "':text='"
306 + infosString + "':fontcolor=" + fontColor_
307 + ":fontsize=" + std::to_string(fontSize_)
308 + ":line_spacing=" + std::to_string(lineSpacing_)
309 + ":box=1:boxcolor=" + fontBackground_ + ":boxborderw=5:x=";
310
311 if (infosposition_ == "1")
312 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
313 + "-text_w:y=" + std::to_string(points_[1].second);
314 else if (infosposition_ == "2")
315 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
316 + ":y=" + std::to_string(points_[1].second);
317 else if (infosposition_ == "3")
318 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
319 + ":y=" + std::to_string(points_[1].second) + "-text_h";
320 else if (infosposition_ == "4")
321 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
322 + "-text_w:y=" + std::to_string(points_[1].second) + "-text_h";
agsantos1bbc7cc2021-05-20 16:43:35 -0400323 infosDescription_ += ",rotate=" + rotation[-angle_] + rotateSides + ",format=yuv420p";
agsantos4bb4bc52021-03-08 14:21:45 -0500324
325 Plog::log(Plog::LogPriority::INFO, TAG, infosDescription_);
326 Plog::log(Plog::LogPriority::INFO, TAG, pluginFilterDescription_);
327}
328
329void
330WatermarkVideoSubscriber::setMarkPosition()
331{
332 // 1, 2, 3, and 4 are cartesian positions
333 int margin = 10;
334 int markWidth = showLogo_ ? mark_->width : 0;
335 int markHeight = showLogo_ ? mark_->height : 0;
336 int infoHeight = (std::abs(angle_) == 90) ? (fontSize_ + lineSpacing_) * infosSize_
337 : lineSpacing_ * 2 + fontSize_;
338 if (pluginFrameSize_.first == 0 || pluginFrameSize_.second == 0)
339 return;
340
341 if (infosposition_ == "1") {
342 points_[1] = {pluginFrameSize_.first - margin, margin};
343 } else if (infosposition_ == "2") {
344 points_[1] = {margin, margin};
345 } else if (infosposition_ == "3") {
346 points_[1] = {margin, pluginFrameSize_.second - margin};
347 } else if (infosposition_ == "4") {
348 points_[1] = {pluginFrameSize_.first - margin, pluginFrameSize_.second - margin};
349 }
350 if (logoposition_ == "1") {
351 points_[0] = {pluginFrameSize_.first - mark_->width - margin / 2, margin};
352 } else if (logoposition_ == "2") {
353 points_[0] = {margin / 2, margin};
354 } else if (logoposition_ == "3") {
355 points_[0] = {margin / 2, pluginFrameSize_.second - markHeight - margin};
356 } else if (logoposition_ == "4") {
357 points_[0] = {pluginFrameSize_.first - markWidth - margin / 2,
358 pluginFrameSize_.second - markHeight - margin};
359 }
360
361 if (infosposition_ == logoposition_ && showInfos_ && showLogo_) {
362 if (logoposition_ == "1" || logoposition_ == "2") {
363 points_[0].second += infoHeight;
364 } else if (logoposition_ == "3" || logoposition_ == "4") {
365 points_[0].second -= infoHeight;
366 }
367 }
368}
369
370void
371WatermarkVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& pluginFrame)
372{
373 if (!observable_ || !pluginFrame || (!validLogo_ && showLogo_))
374 return;
375
376 AVFrameSideData* side_data = av_frame_get_side_data(pluginFrame, AV_FRAME_DATA_DISPLAYMATRIX);
377 int newAngle {0};
378 if (side_data) {
379 auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
380 newAngle = static_cast<int>(av_display_rotation_get(matrix_rotation));
381 }
382 if (newAngle != angle_) {
383 angle_ = newAngle;
384 firstRun = true;
385 }
386
387 //======================================================================================
388 // GET RAW FRAME
agsantos1bbc7cc2021-05-20 16:43:35 -0400389 uniqueFramePtr rgbFrame = {transferToMainMemory(pluginFrame, AV_PIX_FMT_NV12), frameFree};
390 rgbFrame.reset(FrameScaler::convertFormat(rgbFrame.get(), AV_PIX_FMT_YUV420P));
agsantos4bb4bc52021-03-08 14:21:45 -0500391 if (!rgbFrame.get())
392 return;
393 rgbFrame->pts = 1;
394
395 if (firstRun) {
396 pluginFilter_.clean();
397 infosFilter_.clean();
398 pluginFrameSize_ = {rgbFrame->width, rgbFrame->height};
399 if (std::abs(angle_) == 90)
400 pluginFrameSize_ = {rgbFrame->height, rgbFrame->width};
401
402 setFilterDescription();
403
404 rational<int> fr(rgbFrame->pts, 1);
405 pluginstream_ = MediaStream("input",
406 rgbFrame->format,
407 1 / fr,
408 rgbFrame->width,
409 rgbFrame->height,
410 0,
411 fr);
412
413 if (showLogo_) {
414 MediaStream markstream_ = MediaStream("mark",
415 mark_->format,
416 logoStream_.timeBase,
417 mark_->width,
418 mark_->height,
419 0,
420 logoStream_.frameRate);
421 pluginFilter_.initialize(pluginFilterDescription_, {markstream_, pluginstream_});
422 pluginFilter_.feedInput(mark_.get(), "mark");
423 pluginFilter_.feedEOF("mark");
424 }
425
426 infosFilter_.initialize(infosDescription_, {pluginstream_});
427 firstRun = false;
428 }
429
430 if (!infosFilter_.initialized_ && !pluginFilter_.initialized_)
431 return;
432
433 if (showLogo_) {
434 if (pluginFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400435 uniqueFramePtr filteredFrame = {pluginFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500436 if (filteredFrame.get())
437 moveFrom(rgbFrame.get(), filteredFrame.get());
438 }
439 }
440 if (showInfos_) {
441 if (infosFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400442 uniqueFramePtr filteredFrame = {infosFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500443 if (filteredFrame.get())
444 moveFrom(rgbFrame.get(), filteredFrame.get());
445 }
446 }
447 if (showInfos_ || showLogo_) {
448 moveFrom(pluginFrame, rgbFrame.get());
449 }
450}
451
452void
453WatermarkVideoSubscriber::attached(jami::Observable<AVFrame*>* observable)
454{
455 Plog::log(Plog::LogPriority::INFO, TAG, "Attached!");
456 observable_ = observable;
457}
458
459void
460WatermarkVideoSubscriber::detached(jami::Observable<AVFrame*>*)
461{
462 pluginFilter_.clean();
463 infosFilter_.clean();
464 firstRun = true;
465 observable_ = nullptr;
466 Plog::log(Plog::LogPriority::INFO, TAG, "Detached!");
467 mtx_.unlock();
468}
469
470void
471WatermarkVideoSubscriber::detach()
472{
473 if (observable_) {
474 mtx_.lock();
475 firstRun = true;
476 observable_->detach(this);
477 }
478}
479} // namespace jami