blob: e32c2e36ea7844437a86e495d7415b73f24d3929 [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
126 int got_frame;
127 AVCodecContext* pCodecCtx;
agsantos4bb4bc52021-03-08 14:21:45 -0500128 AVPacket* packet;
129
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300130 const AVCodec* pCodec = avcodec_find_decoder(
131 pFormatCtx_->streams[videoStream_]->codecpar->codec_id);
agsantos4bb4bc52021-03-08 14:21:45 -0500132 if (pCodec == nullptr) {
133 pFormatCtx_.reset();
134 Plog::log(Plog::LogPriority::INFO, TAG, "Codec not found.");
135 validLogo_ = false;
136 return;
137 }
138
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300139 pCodecCtx = avcodec_alloc_context3(pCodec);
agsantos4bb4bc52021-03-08 14:21:45 -0500140 // Open codec
141 if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
142 pFormatCtx_.reset();
143 Plog::log(Plog::LogPriority::INFO, TAG, "Could not open codec.");
144 validLogo_ = false;
145 return;
146 }
147
148 packet = av_packet_alloc();
149 if (av_read_frame(pFormatCtx_.get(), packet) < 0) {
150 avcodec_close(pCodecCtx);
151 av_packet_free(&packet);
152 pFormatCtx_.reset();
153 Plog::log(Plog::LogPriority::INFO, TAG, "Could not read packet from context.");
154 validLogo_ = false;
155 return;
156 }
157
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300158 if (avcodec_send_packet(pCodecCtx, packet) < 0) {
159 avcodec_close(pCodecCtx);
160 av_packet_free(&packet);
161 pFormatCtx_.reset();
162 Plog::log(Plog::LogPriority::INFO, TAG, "Could not send packet no codec.");
163 validLogo_ = false;
164 return;
agsantos4bb4bc52021-03-08 14:21:45 -0500165 }
Aline Gondim Santos7763bff2022-07-20 08:04:02 -0300166
167 AVFrame* logoImage = av_frame_alloc();
168 if (avcodec_receive_frame(pCodecCtx, logoImage) < 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 codec.");
173 validLogo_ = false;
174 return;
175 }
176
177 logoFilter_.feedInput(logoImage, "logo");
agsantos4bb4bc52021-03-08 14:21:45 -0500178 logoFilter_.feedEOF("logo");
179
180 frameFree(logoImage);
181 avcodec_close(pCodecCtx);
182 av_packet_free(&packet);
183 pFormatCtx_.reset();
184 mark_.reset(logoFilter_.readOutput());
185 mark_->pts = 0;
186 mark_->best_effort_timestamp = 0;
187 validLogo_ = mark_->width && mark_->height;
188}
189
190void
191WatermarkVideoSubscriber::setParameter(std::string& parameter, Parameter type)
192{
193 switch (type) {
194 case (Parameter::LOGOSIZE):
195 logoSize_ = parameter;
196 break;
197 case (Parameter::TIMEZONE):
198 timeZone_ = parameter == "1";
199 return;
200 case (Parameter::FONTSIZE):
201 fontSize_ = std::stoi(parameter);
202 break;
203 case (Parameter::LOGOBACKGROUND):
204 backgroundColor_ = parameter;
205 if (backgroundColor_.find("black") == std::string::npos) {
206 fontColor_ = "black";
207 fontBackground_ = "white@0.5";
208 } else {
209 fontColor_ = "white";
210 fontBackground_ = "black@0.5";
211 }
212 if (!logoPath_.empty())
213 setParameter(logoPath_, Parameter::LOGOPATH);
214 break;
215 case (Parameter::SHOWLOGO):
216 showLogo_ = parameter == "1";
217 break;
218 case (Parameter::SHOWINFOS):
219 showInfos_ = parameter == "1";
220 break;
221 case (Parameter::LOGOPATH):
222 logoPath_ = parameter;
223 break;
224 case (Parameter::TIME):
225 time_ = parameter == "1";
226 break;
227 case (Parameter::DATE):
228 date_ = parameter == "1";
229 break;
230 case (Parameter::TIMEFORMAT):
231 timeFormat_ = "%{localtime\\:'" + parameter + "'}";
232 if (timeZone_)
233 timeFormat_ = "%{localtime\\:'" + parameter + " %Z'}";
234 break;
235 case (Parameter::DATEFORMAT):
236 dateFormat_ = "%{localtime\\:'" + parameter + "'}";
237 break;
238 case (Parameter::LOCATION):
239 location_ = parameter;
240 break;
241 case (Parameter::INFOSPOSITION):
242 infosposition_ = parameter;
243 break;
244 case (Parameter::LOGOPOSITION):
245 logoposition_ = parameter;
246 break;
247 default:
248 return;
249 }
250
251 firstRun = true;
252}
253
254void
255WatermarkVideoSubscriber::setFilterDescription()
256{
257 loadMarkLogo();
258
259 std::string infoSep = ", ";
260 if (pluginFrameSize_.first < pluginFrameSize_.second)
261 infoSep = ",\n";
262
263 std::vector<std::string> infos;
264 infosSize_ = 0;
265 if (!location_.empty()) {
266 infosSize_++;
267 infos.emplace_back(location_);
268 }
269 if (date_) {
270 infosSize_++;
271 infos.emplace_back(dateFormat_);
272 }
273 if (time_) {
274 infosSize_++;
275 infos.emplace_back(timeFormat_);
276 }
277 infosString.clear();
278 for (int i = 0; i < infosSize_ - 1; i++)
279 infosString += infos[i] + infoSep;
280 if (infosSize_ > 0)
281 infosString += infos.back();
282
283 setMarkPosition();
284
285 std::string rotateSides = "";
286 if (std::abs(angle_) == 90)
287 rotateSides = ":out_w=ih:out_h=iw";
288
289 if (angle_ != 0)
290 pluginFilterDescription_ = "[input]rotate=" + rotation[angle_] + rotateSides
291 + "[rot],[rot][mark]overlay=" + std::to_string(points_[0].first)
292 + ":" + std::to_string(points_[0].second)
293 + ",rotate=" + rotation[-angle_] + rotateSides;
294 else
295 pluginFilterDescription_ = "[input][mark]overlay=" + std::to_string(points_[0].first) + ":"
296 + std::to_string(points_[0].second);
297
298 std::string baseInfosDescription = "[input]rotate=" + rotation[angle_] + rotateSides
299 + ",drawtext=fontfile='" + fontFile_ + "':text='"
300 + infosString + "':fontcolor=" + fontColor_
301 + ":fontsize=" + std::to_string(fontSize_)
302 + ":line_spacing=" + std::to_string(lineSpacing_)
303 + ":box=1:boxcolor=" + fontBackground_ + ":boxborderw=5:x=";
304
305 if (infosposition_ == "1")
306 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
307 + "-text_w:y=" + std::to_string(points_[1].second);
308 else if (infosposition_ == "2")
309 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
310 + ":y=" + std::to_string(points_[1].second);
311 else if (infosposition_ == "3")
312 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
313 + ":y=" + std::to_string(points_[1].second) + "-text_h";
314 else if (infosposition_ == "4")
315 infosDescription_ = baseInfosDescription + std::to_string(points_[1].first)
316 + "-text_w:y=" + std::to_string(points_[1].second) + "-text_h";
agsantos1bbc7cc2021-05-20 16:43:35 -0400317 infosDescription_ += ",rotate=" + rotation[-angle_] + rotateSides + ",format=yuv420p";
agsantos4bb4bc52021-03-08 14:21:45 -0500318
319 Plog::log(Plog::LogPriority::INFO, TAG, infosDescription_);
320 Plog::log(Plog::LogPriority::INFO, TAG, pluginFilterDescription_);
321}
322
323void
324WatermarkVideoSubscriber::setMarkPosition()
325{
326 // 1, 2, 3, and 4 are cartesian positions
327 int margin = 10;
328 int markWidth = showLogo_ ? mark_->width : 0;
329 int markHeight = showLogo_ ? mark_->height : 0;
330 int infoHeight = (std::abs(angle_) == 90) ? (fontSize_ + lineSpacing_) * infosSize_
331 : lineSpacing_ * 2 + fontSize_;
332 if (pluginFrameSize_.first == 0 || pluginFrameSize_.second == 0)
333 return;
334
335 if (infosposition_ == "1") {
336 points_[1] = {pluginFrameSize_.first - margin, margin};
337 } else if (infosposition_ == "2") {
338 points_[1] = {margin, margin};
339 } else if (infosposition_ == "3") {
340 points_[1] = {margin, pluginFrameSize_.second - margin};
341 } else if (infosposition_ == "4") {
342 points_[1] = {pluginFrameSize_.first - margin, pluginFrameSize_.second - margin};
343 }
344 if (logoposition_ == "1") {
345 points_[0] = {pluginFrameSize_.first - mark_->width - margin / 2, margin};
346 } else if (logoposition_ == "2") {
347 points_[0] = {margin / 2, margin};
348 } else if (logoposition_ == "3") {
349 points_[0] = {margin / 2, pluginFrameSize_.second - markHeight - margin};
350 } else if (logoposition_ == "4") {
351 points_[0] = {pluginFrameSize_.first - markWidth - margin / 2,
352 pluginFrameSize_.second - markHeight - margin};
353 }
354
355 if (infosposition_ == logoposition_ && showInfos_ && showLogo_) {
356 if (logoposition_ == "1" || logoposition_ == "2") {
357 points_[0].second += infoHeight;
358 } else if (logoposition_ == "3" || logoposition_ == "4") {
359 points_[0].second -= infoHeight;
360 }
361 }
362}
363
364void
365WatermarkVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& pluginFrame)
366{
367 if (!observable_ || !pluginFrame || (!validLogo_ && showLogo_))
368 return;
369
370 AVFrameSideData* side_data = av_frame_get_side_data(pluginFrame, AV_FRAME_DATA_DISPLAYMATRIX);
371 int newAngle {0};
372 if (side_data) {
373 auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
374 newAngle = static_cast<int>(av_display_rotation_get(matrix_rotation));
375 }
376 if (newAngle != angle_) {
377 angle_ = newAngle;
378 firstRun = true;
379 }
380
381 //======================================================================================
382 // GET RAW FRAME
agsantos1bbc7cc2021-05-20 16:43:35 -0400383 uniqueFramePtr rgbFrame = {transferToMainMemory(pluginFrame, AV_PIX_FMT_NV12), frameFree};
384 rgbFrame.reset(FrameScaler::convertFormat(rgbFrame.get(), AV_PIX_FMT_YUV420P));
agsantos4bb4bc52021-03-08 14:21:45 -0500385 if (!rgbFrame.get())
386 return;
387 rgbFrame->pts = 1;
388
389 if (firstRun) {
390 pluginFilter_.clean();
391 infosFilter_.clean();
392 pluginFrameSize_ = {rgbFrame->width, rgbFrame->height};
393 if (std::abs(angle_) == 90)
394 pluginFrameSize_ = {rgbFrame->height, rgbFrame->width};
395
396 setFilterDescription();
397
398 rational<int> fr(rgbFrame->pts, 1);
399 pluginstream_ = MediaStream("input",
400 rgbFrame->format,
401 1 / fr,
402 rgbFrame->width,
403 rgbFrame->height,
404 0,
405 fr);
406
407 if (showLogo_) {
408 MediaStream markstream_ = MediaStream("mark",
409 mark_->format,
410 logoStream_.timeBase,
411 mark_->width,
412 mark_->height,
413 0,
414 logoStream_.frameRate);
415 pluginFilter_.initialize(pluginFilterDescription_, {markstream_, pluginstream_});
416 pluginFilter_.feedInput(mark_.get(), "mark");
417 pluginFilter_.feedEOF("mark");
418 }
419
420 infosFilter_.initialize(infosDescription_, {pluginstream_});
421 firstRun = false;
422 }
423
424 if (!infosFilter_.initialized_ && !pluginFilter_.initialized_)
425 return;
426
427 if (showLogo_) {
428 if (pluginFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400429 uniqueFramePtr filteredFrame = {pluginFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500430 if (filteredFrame.get())
431 moveFrom(rgbFrame.get(), filteredFrame.get());
432 }
433 }
434 if (showInfos_) {
435 if (infosFilter_.feedInput(rgbFrame.get(), "input") == 0) {
agsantos1bbc7cc2021-05-20 16:43:35 -0400436 uniqueFramePtr filteredFrame = {infosFilter_.readOutput(), frameFree};
agsantos4bb4bc52021-03-08 14:21:45 -0500437 if (filteredFrame.get())
438 moveFrom(rgbFrame.get(), filteredFrame.get());
439 }
440 }
441 if (showInfos_ || showLogo_) {
442 moveFrom(pluginFrame, rgbFrame.get());
443 }
444}
445
446void
447WatermarkVideoSubscriber::attached(jami::Observable<AVFrame*>* observable)
448{
449 Plog::log(Plog::LogPriority::INFO, TAG, "Attached!");
450 observable_ = observable;
451}
452
453void
454WatermarkVideoSubscriber::detached(jami::Observable<AVFrame*>*)
455{
456 pluginFilter_.clean();
457 infosFilter_.clean();
458 firstRun = true;
459 observable_ = nullptr;
460 Plog::log(Plog::LogPriority::INFO, TAG, "Detached!");
461 mtx_.unlock();
462}
463
464void
465WatermarkVideoSubscriber::detach()
466{
467 if (observable_) {
468 mtx_.lock();
469 firstRun = true;
470 observable_->detach(this);
471 }
472}
473} // namespace jami