KPipewire

pipewirerecord.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "pipewirerecord.h"
8#include "encoder_p.h"
9#include "glhelpers.h"
10#include "pipewirerecord_p.h"
11#include <logging_record.h>
12
13#include <QGuiApplication>
14#include <QImage>
15#include <QPainter>
16
17#include <KShell>
18
19#include <unistd.h>
20extern "C" {
21#include <libavcodec/avcodec.h>
22#include <libavformat/avformat.h>
23#include <libavutil/timestamp.h>
24}
25
26#undef av_err2str
27
28#ifdef av_ts2str
29#undef av_ts2str
30char buf[AV_TS_MAX_STRING_SIZE];
31#define av_ts2str(ts) av_ts_make_string(buf, ts)
32#endif // av_ts2str
33
34#ifdef av_ts2timestr
35#undef av_ts2timestr
36char timebuf[AV_TS_MAX_STRING_SIZE];
37#define av_ts2timestr(ts, tb) av_ts_make_time_string(timebuf, ts, tb)
38#endif // av_ts2timestr
39
40static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt)
41{
42 AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
43
44 qCDebug(PIPEWIRERECORD_LOGGING,
45 "pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s "
46 "stream_index:%d",
47 av_ts2str(pkt->pts),
48 av_ts2timestr(pkt->pts, time_base),
49 av_ts2str(pkt->dts),
50 av_ts2timestr(pkt->dts, time_base),
51 av_ts2str(pkt->duration),
52 av_ts2timestr(pkt->duration, time_base),
53 pkt->stream_index);
54}
55
56PipeWireRecord::PipeWireRecord(QObject *parent)
57 : PipeWireBaseEncodedStream(parent)
58 , d(new PipeWireRecordPrivate)
59{
60}
61
62PipeWireRecord::~PipeWireRecord() = default;
63
64void PipeWireRecord::setOutput(const QString &_output)
65{
66 const QString output = KShell::tildeExpand(_output);
67
68 if (d->m_output == output)
69 return;
70
71 d->m_output = output;
72 refresh();
73 Q_EMIT outputChanged(output);
74}
75
76QString PipeWireRecord::output() const
77{
78 return d->m_output;
79}
80
81QString PipeWireRecord::extension() const
82{
83 static QHash<PipeWireBaseEncodedStream::Encoder, QString> s_extensions = {
84 {PipeWireBaseEncodedStream::H264Main, QStringLiteral("mp4")},
85 {PipeWireBaseEncodedStream::H264Baseline, QStringLiteral("mp4")},
86 {PipeWireBaseEncodedStream::VP8, QStringLiteral("webm")},
87 {PipeWireBaseEncodedStream::VP9, QStringLiteral("webm")},
88 {PipeWireBaseEncodedStream::WebP, QStringLiteral("webp")},
89 {PipeWireBaseEncodedStream::Gif, QStringLiteral("gif")},
90 };
91 return s_extensions.value(encoder());
92}
93
94PipeWireRecordProduce::PipeWireRecordProduce(PipeWireBaseEncodedStream::Encoder encoder, uint nodeId, uint fd, const Fraction &framerate, const QString &output)
95 : PipeWireProduce(encoder, nodeId, fd, framerate)
96 , m_output(output)
97{
98}
99
100bool PipeWireRecordProduce::setupFormat()
101{
102 avformat_alloc_output_context2(&m_avFormatContext, nullptr, nullptr, m_output.toUtf8().constData());
103 if (!m_avFormatContext) {
104 qCWarning(PIPEWIRERECORD_LOGGING) << "Could not deduce output format from file: using WebM." << m_output;
105 avformat_alloc_output_context2(&m_avFormatContext, nullptr, "webm", m_output.toUtf8().constData());
106 }
107 if (!m_avFormatContext) {
108 qCDebug(PIPEWIRERECORD_LOGGING) << "could not set stream up";
109 return false;
110 }
111
112 const Fraction framerate = m_stream->framerate();
113 int ret = avio_open(&m_avFormatContext->pb, QFile::encodeName(m_output).constData(), AVIO_FLAG_WRITE);
114 if (ret < 0) {
115 qCWarning(PIPEWIRERECORD_LOGGING) << "Could not open" << m_output << av_err2str(ret);
116 return false;
117 }
118
119 auto avStream = avformat_new_stream(m_avFormatContext, nullptr);
120 avStream->start_time = 0;
121 if (framerate) {
122 avStream->r_frame_rate.num = framerate.numerator;
123 avStream->r_frame_rate.den = framerate.denominator;
124 avStream->avg_frame_rate.num = framerate.numerator;
125 avStream->avg_frame_rate.den = framerate.denominator;
126 }
127
128 ret = avcodec_parameters_from_context(avStream->codecpar, m_encoder->avCodecContext());
129 if (ret < 0) {
130 qCWarning(PIPEWIRERECORD_LOGGING) << "Error occurred when passing the codec:" << av_err2str(ret);
131 return false;
132 }
133
134 AVDictionary *options = nullptr;
135 const auto codecId = m_avFormatContext->oformat->video_codec;
136 if (codecId == AV_CODEC_ID_GIF || codecId == AV_CODEC_ID_WEBP) {
137 av_dict_set_int(&options, "loop", 0, 0);
138 }
139 ret = avformat_write_header(m_avFormatContext, &options);
140 if (ret < 0) {
141 qCWarning(PIPEWIRERECORD_LOGGING) << "Error occurred when writing header:" << av_err2str(ret);
142 return false;
143 }
144
145 return true;
146}
147
148void PipeWireRecordProduce::processFrame(const PipeWireFrame &frame)
149{
150 PipeWireProduce::processFrame(frame);
151 if (frame.cursor && !frame.dmabuf && !frame.dataFrame && m_frameWithoutMetadataCursor.dataFrame) {
152 m_encoder->filterFrame(m_frameWithoutMetadataCursor);
153 }
154}
155
156void PipeWireRecordProduce::aboutToEncode(PipeWireFrame &frame)
157{
158 if (!frame.dataFrame) {
159 return;
160 }
161
162 if (m_cursor.position && !m_cursor.texture.isNull()) {
163 auto image = frame.dataFrame->toImage();
164 // Do not copy the image if it's already ours
165 if (m_frameWithoutMetadataCursor.dataFrame->cleanup != frame.dataFrame->cleanup) {
166 m_frameWithoutMetadataCursor.dataFrame = frame.dataFrame->copy();
167 }
168 QPainter p(&image);
169 p.drawImage(*m_cursor.position, m_cursor.texture);
170 }
171}
172
173void PipeWireRecordProduce::processPacket(AVPacket *packet)
174{
175 packet->stream_index = (*m_avFormatContext->streams)->index;
176 av_packet_rescale_ts(packet, m_encoder->avCodecContext()->time_base, (*m_avFormatContext->streams)->time_base);
177 log_packet(m_avFormatContext, packet);
178 auto ret = av_interleaved_write_frame(m_avFormatContext, packet);
179 if (ret < 0) {
180 qCWarning(PIPEWIRERECORD_LOGGING) << "Error while writing output packet:" << av_err2str(ret);
181 }
182}
183
184std::unique_ptr<PipeWireProduce> PipeWireRecord::makeProduce()
185{
186 return std::make_unique<PipeWireRecordProduce>(encoder(), nodeId(), fd(), maxFramerate(), d->m_output);
187}
188
189int64_t PipeWireRecordProduce::framePts(const std::optional<std::chrono::nanoseconds> &presentationTimestamp)
190{
191 const auto current = std::chrono::duration_cast<std::chrono::milliseconds>(*presentationTimestamp).count();
192 if ((*m_avFormatContext->streams)->start_time == 0) {
193 (*m_avFormatContext->streams)->start_time = current;
194 }
195
196 return current - (*m_avFormatContext->streams)->start_time;
197}
198
199void PipeWireRecordProduce::cleanup()
200{
201 if (m_avFormatContext) {
202 if (auto result = av_write_trailer(m_avFormatContext); result < 0) {
203 qCWarning(PIPEWIRERECORD_LOGGING) << "Could not write trailer";
204 }
205
206 avio_closep(&m_avFormatContext->pb);
207 avformat_free_context(m_avFormatContext);
208 }
209}
210
211#include "moc_pipewirerecord.cpp"
212
213#include "moc_pipewirerecord_p.cpp"
KCOREADDONS_EXPORT QString tildeExpand(const QString &path)
QByteArray encodeName(const QString &fileName)
T value(const Key &key) const const
Q_EMITQ_EMIT
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 7 2025 11:57:23 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.