tgrtmp 0.3.0__cp315-cp315t-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 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