blob: 5296b002793e11e90392c44b91c2985fa736bd2b [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
40WatermarkVideoSubscriber::WatermarkVideoSubscriber(const std::string& dataPath,
41 std::map<std::string, std::string>& preferences)
42{
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");
agsantos4bb4bc52021-03-08 14:21:45 -050048 fontFile_ = dataPath + sep + "Muli-Light.ttf";
49#ifdef WIN32
50 for (int i = fontFile_.size(); i > 0; i--)
51 if (fontFile_[i] == '\\')
52 fontFile_.insert(i, "\\");
53 fontFile_.insert(1, "\\");
54#endif
55 try {
56 setParameter(preferences["fontsize"], Parameter::FONTSIZE);
57 setParameter(preferences["logosize"], Parameter::LOGOSIZE);
58 setParameter(preferences["markbackground"], Parameter::LOGOBACKGROUND);
59 setParameter(preferences["showinfos"], Parameter::SHOWINFOS);
60 setParameter(preferences["showlogo"], Parameter::SHOWLOGO);
61 setParameter(preferences["mark"], Parameter::LOGOPATH);
62 setParameter(preferences["date"], Parameter::DATE);
63 setParameter(preferences["dateformat"], Parameter::DATEFORMAT);
64 setParameter(preferences["time"], Parameter::TIME);
65 setParameter(preferences["timezone"], Parameter::TIMEZONE);
66 setParameter(preferences["timeformat"], Parameter::TIMEFORMAT);
67 setParameter(preferences["location"], Parameter::LOCATION);
68 setParameter(preferences["infosposition"], Parameter::INFOSPOSITION);
69 setParameter(preferences["logoposition"], Parameter::LOGOPOSITION);
70 } catch (std::exception e) {
71 Plog::log(Plog::LogPriority::ERR, TAG, e.what());
72 }
73}
74
75WatermarkVideoSubscriber::~WatermarkVideoSubscriber()
76{
77 validLogo_ = false;
78 logoFilter_.clean();
79 detach();
80 std::lock_guard<std::mutex> lk(mtx_);
81 Plog::log(Plog::LogPriority::INFO, TAG, "~WatermarkMediaProcessor");
82}
83
84MediaStream
85WatermarkVideoSubscriber::getLogoAVFrameInfos()
86{
87 AVFormatContext* ctx = avformat_alloc_context();
88
89 // Open
90 if (avformat_open_input(&ctx, logoPath_.c_str(), NULL, NULL) != 0) {
91 avformat_free_context(ctx);
92 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't open input stream.");
93 return {};
94 }
95 pFormatCtx_.reset(ctx);
96 // Retrieve stream information
97 if (avformat_find_stream_info(pFormatCtx_.get(), NULL) < 0) {
98 Plog::log(Plog::LogPriority::INFO, TAG, "Couldn't find stream information.");
99 return {};
100 }
101
102 // Dump valid information onto standard error
103 av_dump_format(pFormatCtx_.get(), 0, logoPath_.c_str(), false);
104
105 // Find the video stream
106 for (int i = 0; i < static_cast<int>(pFormatCtx_->nb_streams); i++)
107 if (pFormatCtx_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
108 videoStream_ = i;
109 break;
110 }
111
112 if (videoStream_ == -1) {
113 Plog::log(Plog::LogPriority::INFO, TAG, "Didn't find a video stream.");
114 return {};
115 }
116
117 rational<int> fr = pFormatCtx_->streams[videoStream_]->r_frame_rate;
118 return MediaStream("logo",
119 pFormatCtx_->streams[videoStream_]->codecpar->format,
120 1 / fr,
121 pFormatCtx_->streams[videoStream_]->codecpar->width,
122 pFormatCtx_->streams[videoStream_]->codecpar->height,
123 0,
124 fr);
125}
126
127void
128WatermarkVideoSubscriber::loadMarkLogo()
129{
130 if (logoPath_.empty())
131 return;
132
133 logoFilter_.clean();
134 logoDescription_ = "[logo]scale=" + logoSize_ + "*" + std::to_string(pluginFrameSize_.first)
135 + ":" + logoSize_ + "*" + std::to_string(pluginFrameSize_.second)
136 + ":force_original_aspect_ratio='decrease',format=yuva444p,"
137 "split=2[bg][fg],[bg]drawbox=c='"
138 + backgroundColor_
139 + "':replace=1:t=fill[bg],"
140 "[bg][fg]overlay=format=auto";
141 Plog::log(Plog::LogPriority::INFO, TAG, logoDescription_);
142 logoStream_ = getLogoAVFrameInfos();
143 logoFilter_.initialize(logoDescription_, {logoStream_});
144
145 int got_frame;
146 AVCodecContext* pCodecCtx;
147 AVCodec* pCodec;
148 AVPacket* packet;
149
150 pCodecCtx = pFormatCtx_->streams[videoStream_]->codec;
151 pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
152 if (pCodec == nullptr) {
153 pFormatCtx_.reset();
154 Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
155 validLogo_ = false;
156 return;
157 }
158
159 // Open codec
160 if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
161 pFormatCtx_.reset();
162 Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
163 validLogo_ = false;
164 return;
165 }
166
167 packet = av_packet_alloc();
168 if (av_read_frame(pFormatCtx_.get(), packet) < 0) {
169 avcodec_close(pCodecCtx);
170 av_packet_free(&packet);
171 pFormatCtx_.reset();
172 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from context.");
173 validLogo_ = false;
174 return;
175 }
176
177 AVFrame* logoImage = av_frame_alloc();
178 avcodec_decode_video2(pCodecCtx, logoImage, &got_frame, packet);
179 if (got_frame) {
180 logoFilter_.feedInput(logoImage, "logo");
181 }
182 logoFilter_.feedEOF("logo");
183
184 frameFree(logoImage);
185 avcodec_close(pCodecCtx);
186 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 }
216 if (!logoPath_.empty())
217 setParameter(logoPath_, Parameter::LOGOPATH);
218 break;
219 case (Parameter::SHOWLOGO):
220 showLogo_ = parameter == "1";
221 break;
222 case (Parameter::SHOWINFOS):
223 showInfos_ = parameter == "1";
224 break;
225 case (Parameter::LOGOPATH):
226 logoPath_ = parameter;
227 break;
228 case (Parameter::TIME):
229 time_ = parameter == "1";
230 break;
231 case (Parameter::DATE):
232 date_ = parameter == "1";
233 break;
234 case (Parameter::TIMEFORMAT):
235 timeFormat_ = "%{localtime\\:'" + parameter + "'}";
236 if (timeZone_)
237 timeFormat_ = "%{localtime\\:'" + parameter + " %Z'}";
238 break;
239 case (Parameter::DATEFORMAT):
240 dateFormat_ = "%{localtime\\:'" + parameter + "'}";
241 break;
242 case (Parameter::LOCATION):
243 location_ = parameter;
244 break;
245 case (Parameter::INFOSPOSITION):
246 infosposition_ = parameter;
247 break;
248 case (Parameter::LOGOPOSITION):
249 logoposition_ = parameter;
250 break;
251 default:
252 return;
253 }
254
255 firstRun = true;
256}
257
258void
259WatermarkVideoSubscriber::setFilterDescription()
260{
261 loadMarkLogo();
262
263 std::string infoSep = ", ";
264 if (pluginFrameSize_.first < pluginFrameSize_.second)
265 infoSep = ",\n";
266
267 std::vector<std::string> infos;
268 infosSize_ = 0;
269 if (!location_.empty()) {
270 infosSize_++;
271 infos.emplace_back(location_);
272 }
273 if (date_) {
274 infosSize_++;
275 infos.emplace_back(dateFormat_);
276 }
277 if (time_) {
278 infosSize_++;
279 infos.emplace_back(timeFormat_);
280 }
281 infosString.clear();
282 for (int i = 0; i < infosSize_ - 1; i++)
283 infosString += infos[i] + infoSep;
284 if (infosSize_ > 0)
285 infosString += infos.back();
286
287 setMarkPosition();
288
289 std::string rotateSides = "";
290 if (std::abs(angle_) == 90)
291 rotateSides = ":out_w=ih:out_h=iw";
292
293 if (angle_ != 0)
294 pluginFilterDescription_ = "[input]rotate=" + rotation[angle_] + rotateSides
295 + "[rot],[rot][mark]overlay=" + std::to_string(points_[0].first)
296 + ":" + std::to_string(points_[0].second)
297 + ",rotate=" + rotation[-angle_] + rotateSides;
298 else
299 pluginFilterDescription_ = "[input][mark]overlay=" + std::to_string(points_[0].first) + ":"
300 + std::to_string(points_[0].second);
301
302 std::string baseInfosDescription = "[input]rotate=" + rotation[angle_] + rotateSides
303 + ",drawtext=fontfile='" + fontFile_ + "':text='"
304 + infosString + "':fontcolor=" + fontColor_
305 + ":fontsize=" + std::to_string(fontSize_)
306 + ":line_spacing=" + std::to_string(lineSpacing_)
307 + ":box=1:boxcolor=" + fontBackground_ + ":boxborderw=5:x=";
308
309 if (infosposition_ == "1")
310 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
311 + "-text_w:y=" + std::to_string(points_[1].second);
312 else if (infosposition_ == "2")
313 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
314 + ":y=" + std::to_string(points_[1].second);
315 else if (infosposition_ == "3")
316 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
317 + ":y=" + std::to_string(points_[1].second) + "-text_h";
318 else if (infosposition_ == "4")
319 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
320 + "-text_w:y=" + std::to_string(points_[1].second) + "-text_h";
agsantos1bbc7cc2021-05-20 16:43:35 -0400321 infosDescription_ += ",rotate=" + rotation[-angle_] + rotateSides + ",format=yuv420p";
agsantos4bb4bc52021-03-08 14:21:45 -0500322
323 Plog::log(Plog::LogPriority::INFO, TAG, infosDescription_);
324 Plog::log(Plog::LogPriority::INFO, TAG, pluginFilterDescription_);
325}
326
327void
328WatermarkVideoSubscriber::setMarkPosition()
329{
330 // 1, 2, 3, and 4 are cartesian positions
331 int margin = 10;
332 int markWidth = showLogo_ ? mark_->width : 0;
333 int markHeight = showLogo_ ? mark_->height : 0;
334 int infoHeight = (std::abs(angle_) == 90) ? (fontSize_ + lineSpacing_) * infosSize_
335 : lineSpacing_ * 2 + fontSize_;
336 if (pluginFrameSize_.first == 0 || pluginFrameSize_.second == 0)
337 return;
338
339 if (infosposition_ == "1") {
340 points_[1] = {pluginFrameSize_.first - margin, margin};
341 } else if (infosposition_ == "2") {
342 points_[1] = {margin, margin};
343 } else if (infosposition_ == "3") {
344 points_[1] = {margin, pluginFrameSize_.second - margin};
345 } else if (infosposition_ == "4") {
346 points_[1] = {pluginFrameSize_.first - margin, pluginFrameSize_.second - margin};
347 }
348 if (logoposition_ == "1") {
349 points_[0] = {pluginFrameSize_.first - mark_->width - margin / 2, margin};
350 } else if (logoposition_ == "2") {
351 points_[0] = {margin / 2, margin};
352 } else if (logoposition_ == "3") {
353 points_[0] = {margin / 2, pluginFrameSize_.second - markHeight - margin};
354 } else if (logoposition_ == "4") {
355 points_[0] = {pluginFrameSize_.first - markWidth - margin / 2,
356 pluginFrameSize_.second - markHeight - margin};
357 }
358
359 if (infosposition_ == logoposition_ && showInfos_ && showLogo_) {
360 if (logoposition_ == "1" || logoposition_ == "2") {
361 points_[0].second += infoHeight;
362 } else if (logoposition_ == "3" || logoposition_ == "4") {
363 points_[0].second -= infoHeight;
364 }
365 }
366}
367
368void
369WatermarkVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& pluginFrame)
370{
371 if (!observable_ || !pluginFrame || (!validLogo_ && showLogo_))
372 return;
373
374 AVFrameSideData* side_data = av_frame_get_side_data(pluginFrame, AV_FRAME_DATA_DISPLAYMATRIX);
375 int newAngle {0};
376 if (side_data) {
377 auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
378 newAngle = static_cast<int>(av_display_rotation_get(matrix_rotation));
379 }
380 if (newAngle != angle_) {
381 angle_ = newAngle;
382 firstRun = true;
383 }
384
385 //======================================================================================
386 // GET RAW FRAME
agsantos1bbc7cc2021-05-20 16:43:35 -0400387 uniqueFramePtr rgbFrame = {transferToMainMemory(pluginFrame, AV_PIX_FMT_NV12), frameFree};
388 rgbFrame.reset(FrameScaler::convertFormat(rgbFrame.get(), AV_PIX_FMT_YUV420P));
agsantos4bb4bc52021-03-08 14:21:45 -0500389 if (!rgbFrame.get())
390 return;
391 rgbFrame->pts = 1;
392
393 if (firstRun) {
394 pluginFilter_.clean();
395 infosFilter_.clean();
396 pluginFrameSize_ = {rgbFrame->width, rgbFrame->height};
397 if (std::abs(angle_) == 90)
398 pluginFrameSize_ = {rgbFrame->height, rgbFrame->width};
399
400 setFilterDescription();
401
402 rational<int> fr(rgbFrame->pts, 1);
403 pluginstream_ = MediaStream("input",
404 rgbFrame->format,
405 1 / fr,
406 rgbFrame->width,
407 rgbFrame->height,
408 0,
409 fr);
410
411 if (showLogo_) {
412 MediaStream markstream_ = MediaStream("mark",
413 mark_->format,
414 logoStream_.timeBase,
415 mark_->width,
416 mark_->height,
417 0,
418 logoStream_.frameRate);
419 pluginFilter_.initialize(pluginFilterDescription_, {markstream_, pluginstream_});
420 pluginFilter_.feedInput(mark_.get(), "mark");
421 pluginFilter_.feedEOF("mark");
422 }
423
424 infosFilter_.initialize(infosDescription_, {pluginstream_});
425 firstRun = false;
426 }
427
428 if (!infosFilter_.initialized_ && !pluginFilter_.initialized_)
429 return;
430
431 if (showLogo_) {
432 if (pluginFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400433 uniqueFramePtr filteredFrame = {pluginFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500434 if (filteredFrame.get())
435 moveFrom(rgbFrame.get(), filteredFrame.get());
436 }
437 }
438 if (showInfos_) {
439 if (infosFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400440 uniqueFramePtr filteredFrame = {infosFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500441 if (filteredFrame.get())
442 moveFrom(rgbFrame.get(), filteredFrame.get());
443 }
444 }
445 if (showInfos_ || showLogo_) {
446 moveFrom(pluginFrame, rgbFrame.get());
447 }
448}
449
450void
451WatermarkVideoSubscriber::attached(jami::Observable<AVFrame*>* observable)
452{
453 Plog::log(Plog::LogPriority::INFO, TAG, "Attached!");
454 observable_ = observable;
455}
456
457void
458WatermarkVideoSubscriber::detached(jami::Observable<AVFrame*>*)
459{
460 pluginFilter_.clean();
461 infosFilter_.clean();
462 firstRun = true;
463 observable_ = nullptr;
464 Plog::log(Plog::LogPriority::INFO, TAG, "Detached!");
465 mtx_.unlock();
466}
467
468void
469WatermarkVideoSubscriber::detach()
470{
471 if (observable_) {
472 mtx_.lock();
473 firstRun = true;
474 observable_->detach(this);
475 }
476}
477} // namespace jami