@0biwank/screen-capture 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @0biwank/screen-capture might be problematic. Click here for more details.
- package/LICENSE +339 -21
- package/LICENSES/CPP-HTTPLIB-MIT.txt +21 -0
- package/LICENSES/FFMPEG-GPLv2.txt +340 -0
- package/LICENSES/PROJECT-MIT.txt +21 -0
- package/LICENSES/X264-COPYING.txt +341 -0
- package/README.md +53 -64
- package/SOURCE.md +35 -0
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/binding.gyp +58 -0
- package/include/CameraCapturer.h +54 -0
- package/include/DesktopCapturer.h +83 -0
- package/include/HLSMuxer/AudioEncoder.h +75 -0
- package/include/HLSMuxer/FileHLSMuxer.h +63 -0
- package/include/HLSMuxer/HLSMuxer.h +13 -0
- package/include/HLSMuxer/VideoEncoder.h +90 -0
- package/include/MediaPipeline.h +39 -0
- package/include/SourceHelper.h +41 -0
- package/include/SourceHelperWrapper.h +29 -0
- package/include/Types.h +58 -0
- package/include/UploadManager.h +9 -0
- package/include/httplib.h +12065 -0
- package/index.d.ts +99 -0
- package/index.js +105 -14
- package/package.json +31 -17
- package/prebuilds/SHA256SUMS +2 -0
- package/prebuilds/darwin-arm64/native_capture.node +0 -0
- package/prebuilds/darwin-x64/native_capture.node +0 -0
- package/scripts/build-ffmpeg-vendor.mjs +178 -0
- package/scripts/build.mjs +53 -0
- package/scripts/stage-prebuild.mjs +28 -0
- package/scripts/verify-package.mjs +39 -0
- package/scripts/verify-packlist.mjs +40 -0
- package/scripts/verify-runtime.cjs +28 -0
- package/src/CameraCapturer.mm +154 -0
- package/src/DesktopCapturer.mm +995 -0
- package/src/HLSMuxer/AudioEncoder.cpp +484 -0
- package/src/HLSMuxer/FileHLSMuxer.cpp +345 -0
- package/src/HLSMuxer/HLSMuxer.cpp +0 -0
- package/src/HLSMuxer/VideoEncoder.cpp +462 -0
- package/src/MediaPipeline.cpp +375 -0
- package/src/MediaProcessor.cpp +0 -0
- package/src/SourceHelper.mm +184 -0
- package/src/SourceHelperWrapper.mm +63 -0
- package/src/UploadManager.h +7 -0
- package/src/addon.cpp +347 -0
- package/vendor/ffmpeg/README.md +40 -0
- package/build/Release/media_processor.node +0 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#include "HLSMuxer/VideoEncoder.h"
|
|
2
|
+
#include <iostream>
|
|
3
|
+
#include <sstream>
|
|
4
|
+
#include <iomanip>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
extern "C"
|
|
8
|
+
{
|
|
9
|
+
#include <libavcodec/avcodec.h>
|
|
10
|
+
#include <libavformat/avformat.h>
|
|
11
|
+
#include <libswscale/swscale.h>
|
|
12
|
+
#include <libavutil/opt.h>
|
|
13
|
+
#include <libavutil/imgutils.h>
|
|
14
|
+
#include <libavutil/frame.h>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
VideoEncoder::VideoEncoder()
|
|
18
|
+
{
|
|
19
|
+
m_codecContext = nullptr;
|
|
20
|
+
m_codec = nullptr;
|
|
21
|
+
m_frame = nullptr;
|
|
22
|
+
m_swsContext = nullptr;
|
|
23
|
+
m_formatContext = nullptr;
|
|
24
|
+
m_stream = nullptr;
|
|
25
|
+
|
|
26
|
+
m_frameCount = 0;
|
|
27
|
+
m_pts = 0;
|
|
28
|
+
m_dts = 0;
|
|
29
|
+
m_lastInputPts = -1;
|
|
30
|
+
currentChunkIndex = 0;
|
|
31
|
+
|
|
32
|
+
m_initialized = false;
|
|
33
|
+
isPaused = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
VideoEncoder::~VideoEncoder()
|
|
37
|
+
{
|
|
38
|
+
cleanup();
|
|
39
|
+
while (!m_packetQueue.empty())
|
|
40
|
+
{
|
|
41
|
+
AVPacket *pkt = m_packetQueue.front();
|
|
42
|
+
m_packetQueue.pop();
|
|
43
|
+
av_packet_free(&pkt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bool VideoEncoder::initialize(int width, int height, int fps, int bitrate,
|
|
48
|
+
OutputMode outputMode, int gopSize)
|
|
49
|
+
{
|
|
50
|
+
m_codec = avcodec_find_encoder(AV_CODEC_ID_H264);
|
|
51
|
+
if (!m_codec)
|
|
52
|
+
{
|
|
53
|
+
std::cerr << "H264 encoder not found!" << std::endl;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
m_codecContext = avcodec_alloc_context3(m_codec);
|
|
57
|
+
if (!m_codecContext)
|
|
58
|
+
{
|
|
59
|
+
std::cerr << "Could not allocate codec context" << std::endl;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
m_codecContext->bit_rate = bitrate;
|
|
63
|
+
m_codecContext->width = width;
|
|
64
|
+
m_codecContext->height = height;
|
|
65
|
+
m_codecContext->time_base = {1, fps};
|
|
66
|
+
m_codecContext->framerate = {fps, 1};
|
|
67
|
+
m_codecContext->gop_size = gopSize > 0 ? gopSize : fps;
|
|
68
|
+
m_codecContext->max_b_frames = 0;
|
|
69
|
+
m_codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
70
|
+
this->frameWidth = width;
|
|
71
|
+
this->frameHeight = height;
|
|
72
|
+
this->frameRate = fps;
|
|
73
|
+
this->mode = outputMode;
|
|
74
|
+
|
|
75
|
+
AVDictionary *codecOptions = nullptr;
|
|
76
|
+
if (m_codec && m_codec->name && std::string(m_codec->name) == "libx264")
|
|
77
|
+
{
|
|
78
|
+
av_dict_set(&codecOptions, "preset", "veryfast", 0);
|
|
79
|
+
av_dict_set(&codecOptions, "tune", "zerolatency", 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
int ret = avcodec_open2(m_codecContext, m_codec, &codecOptions);
|
|
83
|
+
av_dict_free(&codecOptions);
|
|
84
|
+
if (ret < 0)
|
|
85
|
+
{
|
|
86
|
+
std::cerr << "Could not open codec" << std::endl;
|
|
87
|
+
cleanup();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
m_swsContext = sws_getContext(width, height, AV_PIX_FMT_BGRA, width, height,
|
|
91
|
+
AV_PIX_FMT_YUV420P, SWS_BILINEAR, nullptr,
|
|
92
|
+
nullptr, nullptr);
|
|
93
|
+
if (!m_swsContext)
|
|
94
|
+
{
|
|
95
|
+
std::cerr << "Could not initialize sws context" << std::endl;
|
|
96
|
+
cleanup();
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
m_frame = av_frame_alloc();
|
|
100
|
+
if (!m_frame)
|
|
101
|
+
{
|
|
102
|
+
cleanup();
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
m_frame->format = m_codecContext->pix_fmt;
|
|
106
|
+
m_frame->width = width;
|
|
107
|
+
m_frame->height = height;
|
|
108
|
+
|
|
109
|
+
ret = av_frame_get_buffer(m_frame, 0);
|
|
110
|
+
if (ret < 0)
|
|
111
|
+
{
|
|
112
|
+
std::cerr << "Could not allocate frame buffer" << std::endl;
|
|
113
|
+
cleanup();
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (outputMode == OutputMode::STANDALONE)
|
|
118
|
+
{
|
|
119
|
+
// Initialize format context for TS output
|
|
120
|
+
avformat_alloc_output_context2(&m_formatContext, nullptr, "mpegts", nullptr);
|
|
121
|
+
if (!m_formatContext)
|
|
122
|
+
{
|
|
123
|
+
std::cerr << "Could not create output context for TS" << std::endl;
|
|
124
|
+
cleanup();
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create video stream in the TS container
|
|
129
|
+
m_stream = avformat_new_stream(m_formatContext, nullptr);
|
|
130
|
+
if (!m_stream)
|
|
131
|
+
{
|
|
132
|
+
std::cerr << "Could not create video stream" << std::endl;
|
|
133
|
+
cleanup();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copy codec parameters to stream
|
|
138
|
+
avcodec_parameters_from_context(m_stream->codecpar, m_codecContext);
|
|
139
|
+
m_stream->time_base = m_codecContext->time_base;
|
|
140
|
+
|
|
141
|
+
targetChunkDuration = 10.0; // 10 second chunks by default
|
|
142
|
+
currentSegmentStartPts = 0;
|
|
143
|
+
currentSegmentDuration = 0.0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
m_initialized = true;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
bool VideoEncoder::encodeFrame(uint8_t *pixelData, int stride, int64_t pts)
|
|
151
|
+
{
|
|
152
|
+
if (!m_initialized)
|
|
153
|
+
{
|
|
154
|
+
std::cerr << "Encoder not initalized!" << std::endl;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
int ret = av_frame_make_writable(m_frame);
|
|
159
|
+
if (ret < 0)
|
|
160
|
+
{
|
|
161
|
+
std::cerr << "Frame not writeable" << std::endl;
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const uint8_t *srcData[4] = {pixelData, nullptr, nullptr, nullptr};
|
|
166
|
+
int srcLinesize[4] = {stride, 0, 0, 0};
|
|
167
|
+
sws_scale(
|
|
168
|
+
m_swsContext,
|
|
169
|
+
srcData,
|
|
170
|
+
srcLinesize,
|
|
171
|
+
0,
|
|
172
|
+
frameHeight,
|
|
173
|
+
m_frame->data,
|
|
174
|
+
m_frame->linesize);
|
|
175
|
+
|
|
176
|
+
if (pts == -1)
|
|
177
|
+
{
|
|
178
|
+
m_frame->pts = m_frameCount;
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
{
|
|
182
|
+
m_frame->pts = pts;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (m_frame->pts <= m_lastInputPts)
|
|
186
|
+
{
|
|
187
|
+
m_frame->pts = m_lastInputPts + 1;
|
|
188
|
+
}
|
|
189
|
+
m_lastInputPts = m_frame->pts;
|
|
190
|
+
|
|
191
|
+
m_pts = m_frame->pts;
|
|
192
|
+
|
|
193
|
+
ret = avcodec_send_frame(m_codecContext, m_frame);
|
|
194
|
+
// Reset forced keyframe flag after submission so only one frame gets the IDR hint.
|
|
195
|
+
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
|
|
196
|
+
m_frame->flags &= ~AV_FRAME_FLAG_KEY;
|
|
197
|
+
if (ret < 0)
|
|
198
|
+
{
|
|
199
|
+
std::cerr << "Error sending frame to encode" << std::endl;
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
while (ret >= 0)
|
|
204
|
+
{
|
|
205
|
+
AVPacket *packet = av_packet_alloc();
|
|
206
|
+
ret = avcodec_receive_packet(m_codecContext, packet);
|
|
207
|
+
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
|
|
208
|
+
{
|
|
209
|
+
av_packet_free(&packet);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (mode == OutputMode::MUXED)
|
|
214
|
+
{
|
|
215
|
+
m_packetQueue.push(packet);
|
|
216
|
+
}
|
|
217
|
+
else if (mode == OutputMode::STANDALONE)
|
|
218
|
+
{
|
|
219
|
+
// Check if we need to start a new chunk
|
|
220
|
+
if (shouldStartNewChunk() || currentChunkIndex == 0)
|
|
221
|
+
{
|
|
222
|
+
// Close current chunk if exists
|
|
223
|
+
if (m_formatContext && m_formatContext->pb)
|
|
224
|
+
{
|
|
225
|
+
av_write_trailer(m_formatContext);
|
|
226
|
+
avio_closep(&m_formatContext->pb);
|
|
227
|
+
|
|
228
|
+
currentSegmentDuration = (m_pts - currentSegmentStartPts) * av_q2d(m_codecContext->time_base);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Generate filename for new chunk
|
|
232
|
+
std::ostringstream filename;
|
|
233
|
+
filename << "video_chunk" << std::setfill('0') << std::setw(3) << currentChunkIndex << ".ts";
|
|
234
|
+
std::string chunkFilename = filename.str();
|
|
235
|
+
m_chunkFileNames.push_back(chunkFilename);
|
|
236
|
+
std::cout << "Creating video chunk: " << chunkFilename << std::endl;
|
|
237
|
+
|
|
238
|
+
// Open new chunk file
|
|
239
|
+
if (avio_open(&m_formatContext->pb, chunkFilename.c_str(), AVIO_FLAG_WRITE) < 0)
|
|
240
|
+
{
|
|
241
|
+
std::cerr << "Could not open output file: " << chunkFilename << std::endl;
|
|
242
|
+
av_packet_free(&packet);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Write header for new chunk
|
|
247
|
+
if (avformat_write_header(m_formatContext, nullptr) < 0)
|
|
248
|
+
{
|
|
249
|
+
std::cerr << "Could not write header for chunk: " << chunkFilename << std::endl;
|
|
250
|
+
av_packet_free(&packet);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
currentChunkIndex++;
|
|
255
|
+
currentSegmentStartPts = m_pts;
|
|
256
|
+
|
|
257
|
+
// Force keyframe at chunk boundaries
|
|
258
|
+
forceKeyFrame();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Write packet to current chunk
|
|
262
|
+
packet->stream_index = m_stream->index;
|
|
263
|
+
av_packet_rescale_ts(packet, m_codecContext->time_base, m_stream->time_base);
|
|
264
|
+
av_interleaved_write_frame(m_formatContext, packet);
|
|
265
|
+
av_packet_free(&packet);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
m_frameCount++;
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
void VideoEncoder::cleanup()
|
|
273
|
+
{
|
|
274
|
+
std::cout << "VideoEncoder::cleanup() called, mode=" << (int)mode << ", initialized=" << m_initialized << std::endl;
|
|
275
|
+
|
|
276
|
+
// Flush encoder to get remaining packets and finalize last segment
|
|
277
|
+
if (m_initialized && m_codecContext)
|
|
278
|
+
{
|
|
279
|
+
std::cout << "Flushing encoder..." << std::endl;
|
|
280
|
+
// Send NULL frame to flush encoder
|
|
281
|
+
int flushRet = avcodec_send_frame(m_codecContext, nullptr);
|
|
282
|
+
std::cout << "Flush send returned: " << flushRet << std::endl;
|
|
283
|
+
|
|
284
|
+
// Receive all remaining packets
|
|
285
|
+
while (true)
|
|
286
|
+
{
|
|
287
|
+
AVPacket *packet = av_packet_alloc();
|
|
288
|
+
int ret = avcodec_receive_packet(m_codecContext, packet);
|
|
289
|
+
|
|
290
|
+
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
|
|
291
|
+
{
|
|
292
|
+
av_packet_free(&packet);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (ret < 0)
|
|
297
|
+
{
|
|
298
|
+
av_packet_free(&packet);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (mode == OutputMode::MUXED)
|
|
303
|
+
{
|
|
304
|
+
m_packetQueue.push(packet);
|
|
305
|
+
}
|
|
306
|
+
else if (mode == OutputMode::STANDALONE && m_formatContext)
|
|
307
|
+
{
|
|
308
|
+
// Open first chunk if not already open
|
|
309
|
+
if (!m_formatContext->pb && currentChunkIndex == 0)
|
|
310
|
+
{
|
|
311
|
+
std::ostringstream filename;
|
|
312
|
+
filename << "video_chunk" << std::setfill('0') << std::setw(3) << currentChunkIndex << ".ts";
|
|
313
|
+
std::string chunkFilename = filename.str();
|
|
314
|
+
m_chunkFileNames.push_back(chunkFilename);
|
|
315
|
+
std::cout << "Flush: Creating first video chunk: " << chunkFilename << std::endl;
|
|
316
|
+
|
|
317
|
+
if (avio_open(&m_formatContext->pb, chunkFilename.c_str(), AVIO_FLAG_WRITE) < 0)
|
|
318
|
+
{
|
|
319
|
+
std::cerr << "Could not open output file: " << chunkFilename << std::endl;
|
|
320
|
+
av_packet_free(&packet);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (avformat_write_header(m_formatContext, nullptr) < 0)
|
|
325
|
+
{
|
|
326
|
+
std::cerr << "Flush: Could not write header for output file" << std::endl;
|
|
327
|
+
av_packet_free(&packet);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
currentChunkIndex++;
|
|
332
|
+
currentSegmentStartPts = m_pts;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (m_formatContext->pb)
|
|
336
|
+
{
|
|
337
|
+
std::cout << "Flush: Writing packet to STANDALONE" << std::endl;
|
|
338
|
+
packet->stream_index = m_stream->index;
|
|
339
|
+
av_packet_rescale_ts(packet, m_codecContext->time_base, m_stream->time_base);
|
|
340
|
+
av_interleaved_write_frame(m_formatContext, packet);
|
|
341
|
+
}
|
|
342
|
+
av_packet_free(&packet);
|
|
343
|
+
}
|
|
344
|
+
else
|
|
345
|
+
{
|
|
346
|
+
av_packet_free(&packet);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Finalize last segment if in STANDALONE mode
|
|
351
|
+
if (mode == OutputMode::STANDALONE && m_formatContext && m_formatContext->pb)
|
|
352
|
+
{
|
|
353
|
+
// Update duration for the final segment
|
|
354
|
+
currentSegmentDuration = (m_pts - currentSegmentStartPts) * av_q2d(m_codecContext->time_base);
|
|
355
|
+
|
|
356
|
+
av_write_trailer(m_formatContext);
|
|
357
|
+
avio_closep(&m_formatContext->pb);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (mode != OutputMode::MUXED)
|
|
362
|
+
{
|
|
363
|
+
while (!m_packetQueue.empty())
|
|
364
|
+
{
|
|
365
|
+
AVPacket *pkt = m_packetQueue.front();
|
|
366
|
+
m_packetQueue.pop();
|
|
367
|
+
av_packet_free(&pkt);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (m_frame)
|
|
371
|
+
{
|
|
372
|
+
av_frame_free(&m_frame);
|
|
373
|
+
m_frame = nullptr;
|
|
374
|
+
}
|
|
375
|
+
if (m_swsContext)
|
|
376
|
+
{
|
|
377
|
+
sws_freeContext(m_swsContext);
|
|
378
|
+
m_swsContext = nullptr;
|
|
379
|
+
}
|
|
380
|
+
if (m_codecContext)
|
|
381
|
+
{
|
|
382
|
+
avcodec_free_context(&m_codecContext);
|
|
383
|
+
m_codecContext = nullptr;
|
|
384
|
+
}
|
|
385
|
+
if (m_formatContext)
|
|
386
|
+
{
|
|
387
|
+
if (m_formatContext->pb)
|
|
388
|
+
{
|
|
389
|
+
// Should already be closed above, but ensure it's closed
|
|
390
|
+
avio_closep(&m_formatContext->pb);
|
|
391
|
+
}
|
|
392
|
+
avformat_free_context(m_formatContext);
|
|
393
|
+
m_formatContext = nullptr;
|
|
394
|
+
}
|
|
395
|
+
m_initialized = false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
bool VideoEncoder::hasPackets() const
|
|
399
|
+
{
|
|
400
|
+
return !m_packetQueue.empty();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
AVPacket *VideoEncoder::getNextPacket()
|
|
404
|
+
{
|
|
405
|
+
if (m_packetQueue.empty())
|
|
406
|
+
{
|
|
407
|
+
return nullptr;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
AVPacket *packet = m_packetQueue.front();
|
|
411
|
+
m_packetQueue.pop();
|
|
412
|
+
return packet;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
void VideoEncoder::forceKeyFrame()
|
|
416
|
+
{
|
|
417
|
+
if (m_frame && m_codecContext)
|
|
418
|
+
{
|
|
419
|
+
m_frame->pict_type = AV_PICTURE_TYPE_I;
|
|
420
|
+
m_frame->flags |= AV_FRAME_FLAG_KEY;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
bool VideoEncoder::shouldStartNewChunk() const
|
|
425
|
+
{
|
|
426
|
+
double duration = (m_pts - currentSegmentStartPts) * av_q2d(m_codecContext->time_base);
|
|
427
|
+
return duration >= targetChunkDuration;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
std::string VideoEncoder::generatePlaylist() const
|
|
431
|
+
{
|
|
432
|
+
if (mode != OutputMode::STANDALONE || m_chunkFileNames.empty())
|
|
433
|
+
{
|
|
434
|
+
return "";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
std::string playlist = "#EXTM3U\n";
|
|
438
|
+
playlist += "#EXT-X-VERSION:3\n";
|
|
439
|
+
playlist += "#EXT-X-TARGETDURATION:" + std::to_string(static_cast<int>(targetChunkDuration + 1)) + "\n";
|
|
440
|
+
playlist += "#EXT-X-MEDIA-SEQUENCE:0\n";
|
|
441
|
+
|
|
442
|
+
for (size_t i = 0; i < m_chunkFileNames.size(); ++i)
|
|
443
|
+
{
|
|
444
|
+
// Calculate actual duration for each segment
|
|
445
|
+
double duration = targetChunkDuration; // Default duration
|
|
446
|
+
if (i == m_chunkFileNames.size() - 1 && currentSegmentDuration > 0)
|
|
447
|
+
{
|
|
448
|
+
duration = currentSegmentDuration; // Last segment might be shorter
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
playlist += "#EXTINF:" + std::to_string(duration) + ",\n";
|
|
452
|
+
playlist += m_chunkFileNames[i] + "\n";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Add end list marker if encoding is complete
|
|
456
|
+
if (!m_initialized)
|
|
457
|
+
{
|
|
458
|
+
playlist += "#EXT-X-ENDLIST\n";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return playlist;
|
|
462
|
+
}
|