tgrtmp 0.3.0__cp313-cp313-win_amd64.whl
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.
- tgrtmp/__init__.py +27 -0
- tgrtmp/_core.cp313-win_amd64.pyd +0 -0
- tgrtmp/_version.py +1 -0
- tgrtmp/client.py +305 -0
- tgrtmp/cpp/amf0.cpp +218 -0
- tgrtmp/cpp/amf0.h +105 -0
- tgrtmp/cpp/bindings.cpp +544 -0
- tgrtmp/cpp/chunk.cpp +283 -0
- tgrtmp/cpp/chunk.h +116 -0
- tgrtmp/cpp/client.cpp +726 -0
- tgrtmp/cpp/client.h +152 -0
- tgrtmp/cpp/demux_mkv.cpp +610 -0
- tgrtmp/cpp/demux_mkv.h +76 -0
- tgrtmp/cpp/demux_mp4.cpp +896 -0
- tgrtmp/cpp/demux_mp4.h +116 -0
- tgrtmp/cpp/demux_ts.cpp +317 -0
- tgrtmp/cpp/demux_ts.h +86 -0
- tgrtmp/cpp/flv.cpp +190 -0
- tgrtmp/cpp/flv.h +55 -0
- tgrtmp/cpp/h264.cpp +252 -0
- tgrtmp/cpp/h264.h +92 -0
- tgrtmp/cpp/publisher.cpp +482 -0
- tgrtmp/cpp/publisher.h +221 -0
- tgrtmp/cpp/sink.h +65 -0
- tgrtmp/cpp/source.h +199 -0
- tgrtmp/player.py +762 -0
- tgrtmp-0.3.0.dist-info/DELVEWHEEL +2 -0
- tgrtmp-0.3.0.dist-info/METADATA +133 -0
- tgrtmp-0.3.0.dist-info/RECORD +34 -0
- tgrtmp-0.3.0.dist-info/WHEEL +5 -0
- tgrtmp-0.3.0.dist-info/licenses/LICENSE +21 -0
- tgrtmp.libs/libcrypto-1_1-x64-73769727a37d49411b96e8166b666a2f.dll +0 -0
- tgrtmp.libs/libssl-1_1-x64-9dd7de05da7dd66401e13f728fc5986b.dll +0 -0
- tgrtmp.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
tgrtmp/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""""" # start delvewheel patch
|
|
2
|
+
def _delvewheel_patch_1_13_2():
|
|
3
|
+
import os
|
|
4
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'tgrtmp.libs'))):
|
|
5
|
+
os.add_dll_directory(libs_dir)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_delvewheel_patch_1_13_2()
|
|
9
|
+
del _delvewheel_patch_1_13_2
|
|
10
|
+
# end delvewheel patch
|
|
11
|
+
|
|
12
|
+
from ._core import Client, Source, connect
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .client import Publisher, SourceError, stream
|
|
15
|
+
from .player import Player, publish
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Client",
|
|
19
|
+
"Player",
|
|
20
|
+
"Publisher",
|
|
21
|
+
"Source",
|
|
22
|
+
"SourceError",
|
|
23
|
+
"connect",
|
|
24
|
+
"publish",
|
|
25
|
+
"stream",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
Binary file
|
tgrtmp/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
tgrtmp/client.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
from ._core import Publisher as _Publisher, Source as _Source, connect
|
|
7
|
+
|
|
8
|
+
# A source that ends without producing one frame is broken, and no control
|
|
9
|
+
# path should spin or sit silently on it.
|
|
10
|
+
ZERO_MEDIA = (
|
|
11
|
+
"The source produced no media. If this is an MP4, remux it with its "
|
|
12
|
+
"index in front first: ffmpeg -i input -c copy -movflags +faststart output.mp4"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# The largest moov box a recovery is willing to read.
|
|
16
|
+
_MOOV_CAP = 256 * 1024 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _tail_moov(handle):
|
|
20
|
+
"""Walk the top-level MP4 box headers with seeks; return (moov_bytes,
|
|
21
|
+
moov_offset) when the index sits at the tail, else None. A non-MP4 or a
|
|
22
|
+
non-seekable handle returns None."""
|
|
23
|
+
try:
|
|
24
|
+
handle.seek(0)
|
|
25
|
+
|
|
26
|
+
if handle.read(16)[4:8] != b"ftyp":
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
handle.seek(0, 2)
|
|
30
|
+
size = handle.tell()
|
|
31
|
+
|
|
32
|
+
pos = 0
|
|
33
|
+
|
|
34
|
+
while pos + 8 <= size:
|
|
35
|
+
handle.seek(pos)
|
|
36
|
+
header = handle.read(16)
|
|
37
|
+
|
|
38
|
+
if len(header) < 8:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
box_size = int.from_bytes(header[0:4], "big")
|
|
42
|
+
box_type = header[4:8]
|
|
43
|
+
header_len = 8
|
|
44
|
+
|
|
45
|
+
if box_size == 1:
|
|
46
|
+
if len(header) < 16:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
box_size = int.from_bytes(header[8:16], "big")
|
|
50
|
+
header_len = 16
|
|
51
|
+
|
|
52
|
+
elif box_size == 0:
|
|
53
|
+
box_size = size - pos
|
|
54
|
+
|
|
55
|
+
if box_size < header_len or pos + box_size > size:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
if box_type == b"moov":
|
|
59
|
+
if box_size > _MOOV_CAP:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
handle.seek(pos)
|
|
63
|
+
moov = handle.read(box_size)
|
|
64
|
+
|
|
65
|
+
if len(moov) != box_size:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return moov, pos
|
|
69
|
+
|
|
70
|
+
pos += box_size
|
|
71
|
+
|
|
72
|
+
except (OSError, ValueError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SourceError(RuntimeError):
|
|
79
|
+
"""The published source is broken (bad container, unsupported codec):
|
|
80
|
+
reconnecting cannot help, so :func:`stream` propagates it."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Publisher(_Publisher):
|
|
84
|
+
"""A :class:`Publisher` that keeps its attached client alive."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *args, **kwargs):
|
|
87
|
+
super().__init__(*args, **kwargs)
|
|
88
|
+
self._client = None
|
|
89
|
+
|
|
90
|
+
def attach(self, client):
|
|
91
|
+
self._client = client
|
|
92
|
+
|
|
93
|
+
super().attach(client)
|
|
94
|
+
|
|
95
|
+
def detach(self):
|
|
96
|
+
self._client = None
|
|
97
|
+
|
|
98
|
+
super().detach()
|
|
99
|
+
|
|
100
|
+
def stop(self):
|
|
101
|
+
self._client = None
|
|
102
|
+
|
|
103
|
+
super().stop()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_chunks(source, size=65536):
|
|
107
|
+
# A file-like object, an iterable of bytes, or a zero-argument callable
|
|
108
|
+
# returning bytes until it returns b"" or None.
|
|
109
|
+
read = getattr(source, "read", None)
|
|
110
|
+
|
|
111
|
+
if read is not None:
|
|
112
|
+
while True:
|
|
113
|
+
chunk = read(size)
|
|
114
|
+
|
|
115
|
+
if not chunk:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
yield chunk
|
|
119
|
+
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
iterator = iter(source)
|
|
124
|
+
except TypeError:
|
|
125
|
+
while True:
|
|
126
|
+
chunk = source()
|
|
127
|
+
|
|
128
|
+
if not chunk:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
yield chunk
|
|
132
|
+
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
yield from iterator
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def stream(
|
|
139
|
+
source,
|
|
140
|
+
url,
|
|
141
|
+
key,
|
|
142
|
+
*,
|
|
143
|
+
verify_tls=True,
|
|
144
|
+
connect_timeout=15.0,
|
|
145
|
+
command_timeout=10.0,
|
|
146
|
+
reconnect_delay=1.0,
|
|
147
|
+
max_reconnect_delay=30.0,
|
|
148
|
+
lookahead_ms=1000,
|
|
149
|
+
stop=None,
|
|
150
|
+
log=None,
|
|
151
|
+
):
|
|
152
|
+
"""Publish a live source to an RTMP(S) livestream, reconnecting on drops.
|
|
153
|
+
|
|
154
|
+
:param source: A file-like object (a path, an open file, or a live byte
|
|
155
|
+
stream handle), an iterable of byte chunks, or a callable returning
|
|
156
|
+
chunks until ``b""``.
|
|
157
|
+
:param url: The RTMP application, e.g. ``rtmps://dc4-1.rtmp.t.me/s/``.
|
|
158
|
+
:param stop: An optional zero-argument callable; when it returns True the
|
|
159
|
+
publishing stops.
|
|
160
|
+
:param log: An optional ``callable(message: str)`` for connection notices.
|
|
161
|
+
:return: The :class:`Publisher`, for its statistics.
|
|
162
|
+
|
|
163
|
+
The container of the source is auto-detected (MPEG-TS, MP4
|
|
164
|
+
faststart/fragmented, MKV/WebM); its H.264 video and AAC audio are
|
|
165
|
+
published untouched, paced to real time. Sources in other codecs are
|
|
166
|
+
rejected: Telegram's RTMP server accepts H.264 and AAC only.
|
|
167
|
+
"""
|
|
168
|
+
publisher = Publisher(lookahead_ms=lookahead_ms)
|
|
169
|
+
|
|
170
|
+
# A path is opened here and closed when the publish ends.
|
|
171
|
+
opened = None
|
|
172
|
+
|
|
173
|
+
if isinstance(source, (str, os.PathLike)):
|
|
174
|
+
opened = open(source, "rb")
|
|
175
|
+
source = opened
|
|
176
|
+
|
|
177
|
+
if log is not None:
|
|
178
|
+
# Warnings about what the server will not accept are surfaced through
|
|
179
|
+
# the same log: nothing fails silently.
|
|
180
|
+
publisher.set_logger(lambda message: log(f"warning: {message}"))
|
|
181
|
+
|
|
182
|
+
delay = reconnect_delay
|
|
183
|
+
log = log or (lambda message: None)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
while stop is None or not stop():
|
|
187
|
+
src = None
|
|
188
|
+
client = None
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
started = time.monotonic()
|
|
192
|
+
|
|
193
|
+
client = connect(
|
|
194
|
+
url,
|
|
195
|
+
key,
|
|
196
|
+
verify_tls=verify_tls,
|
|
197
|
+
connect_timeout=connect_timeout,
|
|
198
|
+
command_timeout=command_timeout,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
elapsed_ms = (time.monotonic() - started) * 1000.0
|
|
202
|
+
|
|
203
|
+
publisher.attach(client)
|
|
204
|
+
|
|
205
|
+
log(
|
|
206
|
+
f"Connected to {urlsplit(url).hostname or url} in {elapsed_ms:.0f}ms"
|
|
207
|
+
)
|
|
208
|
+
delay = reconnect_delay
|
|
209
|
+
|
|
210
|
+
src = _Source(publisher)
|
|
211
|
+
|
|
212
|
+
# A broken source is permanent: its errors propagate instead
|
|
213
|
+
# of triggering a reconnect.
|
|
214
|
+
try:
|
|
215
|
+
for chunk in _read_chunks(source):
|
|
216
|
+
if stop is not None and stop():
|
|
217
|
+
return publisher
|
|
218
|
+
|
|
219
|
+
src.feed(chunk)
|
|
220
|
+
|
|
221
|
+
src.finish()
|
|
222
|
+
except RuntimeError as error:
|
|
223
|
+
if client.is_closed:
|
|
224
|
+
raise ConnectionError(str(error)) from error
|
|
225
|
+
|
|
226
|
+
raise SourceError(str(error)) from error
|
|
227
|
+
|
|
228
|
+
# EOF without a single frame is a broken source (an MP4 whose
|
|
229
|
+
# moov index is not faststart reads exactly like this): say so
|
|
230
|
+
# instead of returning a silent zero-frame publish.
|
|
231
|
+
if publisher.frames_sent == 0:
|
|
232
|
+
# A progressive MP4 whose moov sits at the tail streams
|
|
233
|
+
# blind to EOF: read the index, prime the demuxer, and
|
|
234
|
+
# replay the media against absolute offsets.
|
|
235
|
+
found = _tail_moov(source)
|
|
236
|
+
|
|
237
|
+
if found is not None:
|
|
238
|
+
try:
|
|
239
|
+
src.prime(found[0])
|
|
240
|
+
source.seek(0)
|
|
241
|
+
|
|
242
|
+
pos = 0
|
|
243
|
+
|
|
244
|
+
for chunk in _read_chunks(source):
|
|
245
|
+
if stop is not None and stop():
|
|
246
|
+
return publisher
|
|
247
|
+
|
|
248
|
+
src.feed_at(chunk, pos)
|
|
249
|
+
pos += len(chunk)
|
|
250
|
+
|
|
251
|
+
src.finish()
|
|
252
|
+
except RuntimeError as error:
|
|
253
|
+
if client.is_closed:
|
|
254
|
+
raise ConnectionError(str(error)) from error
|
|
255
|
+
|
|
256
|
+
raise SourceError(str(error)) from error
|
|
257
|
+
|
|
258
|
+
if publisher.frames_sent == 0:
|
|
259
|
+
raise SourceError(ZERO_MEDIA)
|
|
260
|
+
|
|
261
|
+
# A finite source: let the pacer flush everything it holds.
|
|
262
|
+
publisher.drain(10.0)
|
|
263
|
+
client.wait_closed(1.0)
|
|
264
|
+
|
|
265
|
+
if stop is None or not stop():
|
|
266
|
+
log("The source ended")
|
|
267
|
+
|
|
268
|
+
return publisher
|
|
269
|
+
|
|
270
|
+
except SourceError:
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
except (RuntimeError, ConnectionError, OSError) as error:
|
|
274
|
+
if stop is not None and stop():
|
|
275
|
+
return publisher
|
|
276
|
+
|
|
277
|
+
log(
|
|
278
|
+
f"The RTMP connection dropped ({error}); retrying in "
|
|
279
|
+
f"{delay:.0f} seconds..."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if stop is None:
|
|
283
|
+
time.sleep(delay)
|
|
284
|
+
else:
|
|
285
|
+
for _ in range(int(delay * 10)):
|
|
286
|
+
if stop():
|
|
287
|
+
return publisher
|
|
288
|
+
|
|
289
|
+
time.sleep(0.1)
|
|
290
|
+
|
|
291
|
+
delay = min(delay * 2, max_reconnect_delay)
|
|
292
|
+
|
|
293
|
+
finally:
|
|
294
|
+
publisher.detach()
|
|
295
|
+
|
|
296
|
+
if client is not None:
|
|
297
|
+
client.close()
|
|
298
|
+
|
|
299
|
+
finally:
|
|
300
|
+
publisher.stop()
|
|
301
|
+
|
|
302
|
+
if opened is not None:
|
|
303
|
+
opened.close()
|
|
304
|
+
|
|
305
|
+
return publisher
|
tgrtmp/cpp/amf0.cpp
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#include "amf0.h"
|
|
2
|
+
|
|
3
|
+
#include <cstring>
|
|
4
|
+
|
|
5
|
+
namespace tgrtmp::amf0 {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
constexpr uint8_t MARKER_NUMBER = 0x00;
|
|
10
|
+
constexpr uint8_t MARKER_BOOLEAN = 0x01;
|
|
11
|
+
constexpr uint8_t MARKER_STRING = 0x02;
|
|
12
|
+
constexpr uint8_t MARKER_OBJECT = 0x03;
|
|
13
|
+
constexpr uint8_t MARKER_NULL = 0x05;
|
|
14
|
+
constexpr uint8_t MARKER_UNDEFINED = 0x06;
|
|
15
|
+
constexpr uint8_t MARKER_ECMA_ARRAY = 0x08;
|
|
16
|
+
constexpr uint8_t MARKER_OBJECT_END = 0x09;
|
|
17
|
+
constexpr uint8_t MARKER_STRICT_ARRAY = 0x0A;
|
|
18
|
+
constexpr uint8_t MARKER_DATE = 0x0B;
|
|
19
|
+
constexpr uint8_t MARKER_LONG_STRING = 0x0C;
|
|
20
|
+
|
|
21
|
+
// Doubles are stored big-endian in AMF0.
|
|
22
|
+
void put_be64(std::string &out, double value) {
|
|
23
|
+
uint64_t bits;
|
|
24
|
+
std::memcpy(&bits, &value, 8);
|
|
25
|
+
for (int i = 7; i >= 0; --i)
|
|
26
|
+
out.push_back(static_cast<char>((bits >> (8 * i)) & 0xFF));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void put_be32(std::string &out, uint32_t v) {
|
|
30
|
+
out.push_back(static_cast<char>((v >> 24) & 0xFF));
|
|
31
|
+
out.push_back(static_cast<char>((v >> 16) & 0xFF));
|
|
32
|
+
out.push_back(static_cast<char>((v >> 8) & 0xFF));
|
|
33
|
+
out.push_back(static_cast<char>(v & 0xFF));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
void put_be16(std::string &out, uint32_t v) {
|
|
37
|
+
out.push_back(static_cast<char>((v >> 8) & 0xFF));
|
|
38
|
+
out.push_back(static_cast<char>(v & 0xFF));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
struct Decoder {
|
|
42
|
+
std::string_view data;
|
|
43
|
+
size_t offset = 0;
|
|
44
|
+
|
|
45
|
+
[[noreturn]] static void fail(const char *what) { throw std::runtime_error(what); }
|
|
46
|
+
|
|
47
|
+
uint8_t u8() {
|
|
48
|
+
if (offset >= data.size())
|
|
49
|
+
fail("truncated AMF0 data");
|
|
50
|
+
return static_cast<uint8_t>(data[offset++]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
uint32_t u24() {
|
|
54
|
+
return (static_cast<uint32_t>(u8()) << 16) | (static_cast<uint32_t>(u8()) << 8) | u8();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
uint32_t u32() {
|
|
58
|
+
uint32_t v = 0;
|
|
59
|
+
for (int i = 0; i < 4; ++i)
|
|
60
|
+
v = (v << 8) | u8();
|
|
61
|
+
return v;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
uint32_t u16() { return (static_cast<uint32_t>(u8()) << 8) | u8(); }
|
|
65
|
+
|
|
66
|
+
double f64() {
|
|
67
|
+
uint64_t bits = 0;
|
|
68
|
+
for (int i = 0; i < 8; ++i)
|
|
69
|
+
bits = (bits << 8) | u8();
|
|
70
|
+
double d;
|
|
71
|
+
std::memcpy(&d, &bits, 8);
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
std::string_view take(size_t length) {
|
|
76
|
+
if (length > data.size() - offset)
|
|
77
|
+
fail("truncated AMF0 data");
|
|
78
|
+
std::string_view out = data.substr(offset, length);
|
|
79
|
+
offset += length;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::string take_string() {
|
|
84
|
+
uint32_t length = u16();
|
|
85
|
+
return std::string(take(length));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void decode_properties(std::vector<std::pair<std::string, AmfValue>> &out) {
|
|
89
|
+
while (true) {
|
|
90
|
+
if (offset + 2 > data.size())
|
|
91
|
+
fail("truncated AMF0 properties");
|
|
92
|
+
uint32_t key_length = (static_cast<uint32_t>(static_cast<uint8_t>(data[offset])) << 8) |
|
|
93
|
+
static_cast<uint8_t>(data[offset + 1]);
|
|
94
|
+
if (key_length == 0) {
|
|
95
|
+
// Object end: 0x00 0x00 followed by the object-end marker.
|
|
96
|
+
if (offset + 3 > data.size())
|
|
97
|
+
fail("truncated AMF0 object end");
|
|
98
|
+
if (static_cast<uint8_t>(data[offset + 2]) != MARKER_OBJECT_END) {
|
|
99
|
+
fail("missing AMF0 object end marker");
|
|
100
|
+
}
|
|
101
|
+
offset += 3;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
offset += 2;
|
|
105
|
+
std::string key(take(key_length));
|
|
106
|
+
out.emplace_back(std::move(key), decode_value());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
AmfValue decode_value() {
|
|
111
|
+
switch (uint8_t marker = u8(); marker) {
|
|
112
|
+
case MARKER_NUMBER:
|
|
113
|
+
return AmfValue(f64());
|
|
114
|
+
case MARKER_BOOLEAN:
|
|
115
|
+
return AmfValue(u8() != 0);
|
|
116
|
+
case MARKER_STRING:
|
|
117
|
+
return AmfValue(take_string());
|
|
118
|
+
case MARKER_LONG_STRING:
|
|
119
|
+
return AmfValue(std::string(take(u32())));
|
|
120
|
+
case MARKER_NULL:
|
|
121
|
+
case MARKER_UNDEFINED:
|
|
122
|
+
return AmfValue();
|
|
123
|
+
case MARKER_ECMA_ARRAY: {
|
|
124
|
+
u32(); // The associative-count is only a hint.
|
|
125
|
+
AmfObject obj(true, {});
|
|
126
|
+
decode_properties(obj.entries);
|
|
127
|
+
return AmfValue(std::move(obj));
|
|
128
|
+
}
|
|
129
|
+
case MARKER_OBJECT: {
|
|
130
|
+
AmfObject obj(false, {});
|
|
131
|
+
decode_properties(obj.entries);
|
|
132
|
+
return AmfValue(std::move(obj));
|
|
133
|
+
}
|
|
134
|
+
case MARKER_STRICT_ARRAY: {
|
|
135
|
+
uint32_t count = u32();
|
|
136
|
+
std::vector<AmfValue> list;
|
|
137
|
+
list.reserve(count);
|
|
138
|
+
for (uint32_t i = 0; i < count; ++i)
|
|
139
|
+
list.push_back(decode_value());
|
|
140
|
+
return AmfValue(std::move(list));
|
|
141
|
+
}
|
|
142
|
+
case MARKER_DATE: {
|
|
143
|
+
double ms = f64();
|
|
144
|
+
u16(); // Time zone, unused (reserved to be 0).
|
|
145
|
+
return AmfValue(ms);
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
fail("unsupported AMF0 marker");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
void encode_properties(std::string &out, const AmfObject &obj) {
|
|
154
|
+
for (const auto &[key, value] : obj.entries) {
|
|
155
|
+
put_be16(out, static_cast<uint32_t>(key.size()));
|
|
156
|
+
out.append(key);
|
|
157
|
+
encode(out, value);
|
|
158
|
+
}
|
|
159
|
+
out.push_back('\x00');
|
|
160
|
+
out.push_back('\x00');
|
|
161
|
+
out.push_back(static_cast<char>(MARKER_OBJECT_END));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} // namespace
|
|
165
|
+
|
|
166
|
+
void encode(std::string &out, const AmfValue &value) {
|
|
167
|
+
if (auto *b = std::get_if<bool>(&value.v)) {
|
|
168
|
+
out.push_back(static_cast<char>(MARKER_BOOLEAN));
|
|
169
|
+
out.push_back(*b ? '\x01' : '\x00');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (auto *d = std::get_if<double>(&value.v)) {
|
|
173
|
+
out.push_back(static_cast<char>(MARKER_NUMBER));
|
|
174
|
+
put_be64(out, *d);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (auto *s = std::get_if<std::string>(&value.v)) {
|
|
178
|
+
if (s->size() > 0xFFFF) {
|
|
179
|
+
out.push_back(static_cast<char>(MARKER_LONG_STRING));
|
|
180
|
+
put_be32(out, static_cast<uint32_t>(s->size()));
|
|
181
|
+
} else {
|
|
182
|
+
out.push_back(static_cast<char>(MARKER_STRING));
|
|
183
|
+
put_be16(out, static_cast<uint32_t>(s->size()));
|
|
184
|
+
}
|
|
185
|
+
out.append(*s);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (auto *list = std::get_if<std::vector<AmfValue>>(&value.v)) {
|
|
189
|
+
out.push_back(static_cast<char>(MARKER_STRICT_ARRAY));
|
|
190
|
+
put_be32(out, static_cast<uint32_t>(list->size()));
|
|
191
|
+
for (const auto &item : *list)
|
|
192
|
+
encode(out, item);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (auto *obj = std::get_if<AmfObject>(&value.v)) {
|
|
196
|
+
if (obj->ecma) {
|
|
197
|
+
out.push_back(static_cast<char>(MARKER_ECMA_ARRAY));
|
|
198
|
+
put_be32(out, static_cast<uint32_t>(obj->entries.size()));
|
|
199
|
+
} else {
|
|
200
|
+
out.push_back(static_cast<char>(MARKER_OBJECT));
|
|
201
|
+
}
|
|
202
|
+
encode_properties(out, *obj);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// std::monostate: null and undefined both decode to it; encode null.
|
|
206
|
+
out.push_back(static_cast<char>(MARKER_NULL));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
std::vector<AmfValue> decode(std::string_view data) {
|
|
210
|
+
Decoder decoder{data, 0};
|
|
211
|
+
std::vector<AmfValue> values;
|
|
212
|
+
while (decoder.offset < data.size()) {
|
|
213
|
+
values.push_back(decoder.decode_value());
|
|
214
|
+
}
|
|
215
|
+
return values;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
} // namespace tgrtmp::amf0
|
tgrtmp/cpp/amf0.h
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// AMF0 (Action Message Format 0) encoder/decoder for the RTMP command and
|
|
2
|
+
// metadata messages, per the Adobe AMF0 spec (§2.1 of "Action Message Format
|
|
3
|
+
// -- AMF 0", 2006). Only the marker types RTMP servers actually use are
|
|
4
|
+
// supported: number, boolean, string, object, null, undefined, ECMA array,
|
|
5
|
+
// strict array, date and long string.
|
|
6
|
+
//
|
|
7
|
+
// This is a clean-room implementation written against the public spec.
|
|
8
|
+
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#include <cstdint>
|
|
12
|
+
#include <stdexcept>
|
|
13
|
+
#include <string>
|
|
14
|
+
#include <string_view>
|
|
15
|
+
#include <utility>
|
|
16
|
+
#include <variant>
|
|
17
|
+
#include <vector>
|
|
18
|
+
|
|
19
|
+
namespace tgrtmp {
|
|
20
|
+
|
|
21
|
+
struct AmfValue;
|
|
22
|
+
|
|
23
|
+
// An ordered string -> value map. Encoded as an AMF0 ECMA array (0x08) when
|
|
24
|
+
// `ecma` is true (the type of @setDataFrame's onMetaData), otherwise as an
|
|
25
|
+
// AMF0 object (0x03).
|
|
26
|
+
struct AmfObject {
|
|
27
|
+
bool ecma = false;
|
|
28
|
+
// AmfValue is incomplete here; std::vector tolerates incomplete value
|
|
29
|
+
// types as long as it is completed before members are used (C++17).
|
|
30
|
+
std::vector<std::pair<std::string, AmfValue>> entries;
|
|
31
|
+
|
|
32
|
+
AmfObject() = default;
|
|
33
|
+
AmfObject(bool ecma_, std::vector<std::pair<std::string, AmfValue>> entries_)
|
|
34
|
+
: ecma(ecma_), entries(std::move(entries_)) {}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
struct AmfValue {
|
|
38
|
+
std::variant<std::monostate, bool, double, std::string, std::vector<AmfValue>, AmfObject> v;
|
|
39
|
+
|
|
40
|
+
AmfValue() = default;
|
|
41
|
+
AmfValue(std::monostate) {}
|
|
42
|
+
AmfValue(bool b) : v(b) {}
|
|
43
|
+
AmfValue(double d) : v(d) {}
|
|
44
|
+
AmfValue(int i) : v(static_cast<double>(i)) {}
|
|
45
|
+
AmfValue(const char *s) : v(std::string(s)) {}
|
|
46
|
+
AmfValue(std::string s) : v(std::move(s)) {}
|
|
47
|
+
AmfValue(std::string_view s) : v(std::string(s)) {}
|
|
48
|
+
AmfValue(std::vector<AmfValue> list) : v(std::move(list)) {}
|
|
49
|
+
AmfValue(AmfObject obj) : v(std::move(obj)) {}
|
|
50
|
+
|
|
51
|
+
bool is_null() const { return std::holds_alternative<std::monostate>(v); }
|
|
52
|
+
bool is_number() const { return std::holds_alternative<double>(v); }
|
|
53
|
+
bool is_string() const { return std::holds_alternative<std::string>(v); }
|
|
54
|
+
bool is_object() const { return std::holds_alternative<AmfObject>(v); }
|
|
55
|
+
|
|
56
|
+
double as_number(double fallback = 0.0) const {
|
|
57
|
+
if (auto *d = std::get_if<double>(&v))
|
|
58
|
+
return *d;
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
const std::string *as_string() const { return std::get_if<std::string>(&v); }
|
|
62
|
+
const AmfObject *as_object() const { return std::get_if<AmfObject>(&v); }
|
|
63
|
+
|
|
64
|
+
// Object/ECMA property lookup by key.
|
|
65
|
+
const AmfValue *find(std::string_view key) const {
|
|
66
|
+
if (auto *obj = std::get_if<AmfObject>(&v)) {
|
|
67
|
+
for (const auto &[k, val] : obj->entries) {
|
|
68
|
+
if (k == key)
|
|
69
|
+
return &val;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return nullptr;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
namespace amf0 {
|
|
77
|
+
|
|
78
|
+
// Append the AMF0 encoding of `value` to `out`.
|
|
79
|
+
void encode(std::string &out, const AmfValue &value);
|
|
80
|
+
|
|
81
|
+
// Encode several values back to back, as the arguments of a command message.
|
|
82
|
+
template <typename... Rest>
|
|
83
|
+
void encode_args(std::string &out, const AmfValue &first, const Rest &...rest) {
|
|
84
|
+
encode(out, first);
|
|
85
|
+
(encode(out, rest), ...);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Decode every value of a payload. Throws std::runtime_error on malformed or
|
|
89
|
+
// truncated data, or on unsupported markers.
|
|
90
|
+
std::vector<AmfValue> decode(std::string_view data);
|
|
91
|
+
|
|
92
|
+
inline AmfValue null_value() { return AmfValue(); }
|
|
93
|
+
inline AmfValue boolean(bool b) { return AmfValue(b); }
|
|
94
|
+
inline AmfValue number(double d) { return AmfValue(d); }
|
|
95
|
+
inline AmfValue string(std::string_view s) { return AmfValue(s); }
|
|
96
|
+
|
|
97
|
+
inline AmfValue object(std::vector<std::pair<std::string, AmfValue>> entries) {
|
|
98
|
+
return AmfValue(AmfObject(false, std::move(entries)));
|
|
99
|
+
}
|
|
100
|
+
inline AmfValue ecma_array(std::vector<std::pair<std::string, AmfValue>> entries) {
|
|
101
|
+
return AmfValue(AmfObject(true, std::move(entries)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
} // namespace amf0
|
|
105
|
+
} // namespace tgrtmp
|