vidformer 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.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.
- vidformer/__init__.py +1 -1
- vidformer/cv2/vf_cv2.py +152 -11
- vidformer/igni/__init__.py +1 -0
- vidformer/igni/vf_igni.py +285 -0
- vidformer/vf.py +128 -0
- {vidformer-0.8.0.dist-info → vidformer-0.9.0.dist-info}/METADATA +1 -1
- vidformer-0.9.0.dist-info/RECORD +9 -0
- vidformer-0.8.0.dist-info/RECORD +0 -7
- {vidformer-0.8.0.dist-info → vidformer-0.9.0.dist-info}/WHEEL +0 -0
vidformer/__init__.py
CHANGED
vidformer/cv2/vf_cv2.py
CHANGED
@@ -11,6 +11,7 @@ vidformer.cv2 is the cv2 frontend for [vidformer](https://github.com/ixlab/vidfo
|
|
11
11
|
"""
|
12
12
|
|
13
13
|
from .. import vf
|
14
|
+
from .. import igni
|
14
15
|
|
15
16
|
try:
|
16
17
|
import cv2 as _opencv2
|
@@ -22,6 +23,8 @@ import numpy as np
|
|
22
23
|
import uuid
|
23
24
|
from fractions import Fraction
|
24
25
|
from bisect import bisect_right
|
26
|
+
import zlib
|
27
|
+
import re
|
25
28
|
|
26
29
|
CAP_PROP_POS_MSEC = 0
|
27
30
|
CAP_PROP_POS_FRAMES = 1
|
@@ -78,10 +81,10 @@ def _server():
|
|
78
81
|
return _global_cv2_server
|
79
82
|
|
80
83
|
|
81
|
-
def set_cv2_server(server
|
84
|
+
def set_cv2_server(server):
|
82
85
|
"""Set the server to use for the cv2 frontend."""
|
83
86
|
global _global_cv2_server
|
84
|
-
assert isinstance(server, vf.YrdenServer)
|
87
|
+
assert isinstance(server, vf.YrdenServer) or isinstance(server, igni.IgniServer)
|
85
88
|
_global_cv2_server = server
|
86
89
|
|
87
90
|
|
@@ -210,27 +213,59 @@ class Frame:
|
|
210
213
|
|
211
214
|
|
212
215
|
def _inline_frame(arr):
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
+
if arr.dtype != np.uint8:
|
217
|
+
raise Exception("Only uint8 arrays are supported")
|
218
|
+
if len(arr.shape) != 3:
|
219
|
+
raise Exception("Only 3D arrays are supported")
|
220
|
+
if arr.shape[2] != 3:
|
221
|
+
raise Exception("To inline a frame, the array must have 3 channels")
|
216
222
|
|
217
|
-
# convert BGR to RGB
|
218
223
|
arr = arr[:, :, ::-1]
|
224
|
+
if not arr.flags["C_CONTIGUOUS"]:
|
225
|
+
arr = np.ascontiguousarray(arr)
|
219
226
|
|
220
227
|
width = arr.shape[1]
|
221
228
|
height = arr.shape[0]
|
222
229
|
pix_fmt = "rgb24"
|
223
230
|
|
224
|
-
|
231
|
+
data_gzip = zlib.compress(memoryview(arr), level=1)
|
232
|
+
|
233
|
+
f = _inline_mat(
|
234
|
+
data_gzip, width=width, height=height, pix_fmt=pix_fmt, compression="zlib"
|
235
|
+
)
|
225
236
|
fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
|
237
|
+
|
238
|
+
# Return the resulting Frame object
|
226
239
|
return Frame(f, fmt)
|
227
240
|
|
228
241
|
|
229
242
|
class VideoCapture:
|
230
243
|
def __init__(self, path):
|
231
|
-
self._path = path
|
232
244
|
server = _server()
|
233
|
-
|
245
|
+
if type(path) == str:
|
246
|
+
if isinstance(server, vf.YrdenServer):
|
247
|
+
self._path = path
|
248
|
+
self._source = vf.Source(server, str(uuid.uuid4()), path, 0)
|
249
|
+
else:
|
250
|
+
assert isinstance(server, igni.IgniServer)
|
251
|
+
match = re.match(r"(http|https)://([^/]+)(.*)", path)
|
252
|
+
if match is not None:
|
253
|
+
endpoint = f"{match.group(1)}://{match.group(2)}"
|
254
|
+
path = match.group(3)
|
255
|
+
if path.startswith("/"):
|
256
|
+
path = path[1:]
|
257
|
+
self._path = path
|
258
|
+
self._source = server.source(
|
259
|
+
path, 0, "http", {"endpoint": endpoint}
|
260
|
+
)
|
261
|
+
else:
|
262
|
+
raise Exception(
|
263
|
+
"Using a VideoCapture source by name only works with http(s) URLs. You need to pass an IgniSource instead."
|
264
|
+
)
|
265
|
+
elif isinstance(path, igni.IgniSource):
|
266
|
+
assert isinstance(server, igni.IgniServer)
|
267
|
+
self._path = path._name
|
268
|
+
self._source = path
|
234
269
|
self._next_frame_idx = 0
|
235
270
|
|
236
271
|
def isOpened(self):
|
@@ -263,7 +298,7 @@ class VideoCapture:
|
|
263
298
|
raise Exception(f"Unsupported property {prop}")
|
264
299
|
|
265
300
|
def read(self):
|
266
|
-
if self._next_frame_idx >= len(self._source
|
301
|
+
if self._next_frame_idx >= len(self._source):
|
267
302
|
return False, None
|
268
303
|
frame = self._source.iloc[self._next_frame_idx]
|
269
304
|
self._next_frame_idx += 1
|
@@ -275,6 +310,107 @@ class VideoCapture:
|
|
275
310
|
|
276
311
|
|
277
312
|
class VideoWriter:
|
313
|
+
def __init__(self, *args, **kwargs):
|
314
|
+
server = _server()
|
315
|
+
if isinstance(server, vf.YrdenServer):
|
316
|
+
self._writer = _YrdenVideoWriter(*args, **kwargs)
|
317
|
+
elif isinstance(server, igni.IgniServer):
|
318
|
+
self._writer = _IgniVideoWriter(*args, **kwargs)
|
319
|
+
else:
|
320
|
+
raise Exception("Unsupported server type")
|
321
|
+
|
322
|
+
def spec(self):
|
323
|
+
return self._writer.spec()
|
324
|
+
|
325
|
+
def write(self, *args, **kwargs):
|
326
|
+
return self._writer.write(*args, **kwargs)
|
327
|
+
|
328
|
+
def release(self, *args, **kwargs):
|
329
|
+
return self._writer.release(*args, **kwargs)
|
330
|
+
|
331
|
+
def spec(self, *args, **kwargs):
|
332
|
+
return self._writer.spec(*args, **kwargs)
|
333
|
+
|
334
|
+
|
335
|
+
class _IgniVideoWriter:
|
336
|
+
def __init__(
|
337
|
+
self,
|
338
|
+
path,
|
339
|
+
_fourcc,
|
340
|
+
fps,
|
341
|
+
size,
|
342
|
+
batch_size=1024,
|
343
|
+
vod_segment_length=Fraction(2, 1),
|
344
|
+
):
|
345
|
+
server = _server()
|
346
|
+
assert isinstance(server, igni.IgniServer)
|
347
|
+
if path is not None:
|
348
|
+
raise Exception(
|
349
|
+
"Igni does not support writing to a file. VideoWriter path must be None"
|
350
|
+
)
|
351
|
+
if isinstance(fps, int):
|
352
|
+
self._f_time = Fraction(1, fps)
|
353
|
+
elif isinstance(fps, Fraction):
|
354
|
+
self._f_time = 1 / fps
|
355
|
+
else:
|
356
|
+
raise Exception("fps must be an integer or a Fraction")
|
357
|
+
|
358
|
+
assert isinstance(size, tuple) or isinstance(size, list)
|
359
|
+
assert len(size) == 2
|
360
|
+
width, height = size
|
361
|
+
self._spec = server.create_spec(
|
362
|
+
width, height, "yuv420p", vod_segment_length, 1 / self._f_time
|
363
|
+
)
|
364
|
+
self._batch_size = batch_size
|
365
|
+
self._idx = 0
|
366
|
+
self._frame_buffer = []
|
367
|
+
|
368
|
+
def _flush(self, terminal=False):
|
369
|
+
server = _server()
|
370
|
+
server.push_spec_part(
|
371
|
+
self._spec,
|
372
|
+
self._idx - len(self._frame_buffer),
|
373
|
+
self._frame_buffer,
|
374
|
+
terminal=terminal,
|
375
|
+
)
|
376
|
+
self._frame_buffer = []
|
377
|
+
|
378
|
+
def _explicit_terminate(self):
|
379
|
+
server = _server()
|
380
|
+
server.push_spec_part(self._spec._id, self._idx, [], terminal=True)
|
381
|
+
|
382
|
+
def spec(self):
|
383
|
+
return self._spec
|
384
|
+
|
385
|
+
def write(self, frame):
|
386
|
+
if frame is not None:
|
387
|
+
frame = frameify(frame, "frame")
|
388
|
+
if frame._fmt["width"] != self._spec._fmt["width"]:
|
389
|
+
raise Exception(
|
390
|
+
f"Frame type error; expected width {self._spec._fmt['width']}, got {frame._fmt['width']}"
|
391
|
+
)
|
392
|
+
if frame._fmt["height"] != self._spec._fmt["height"]:
|
393
|
+
raise Exception(
|
394
|
+
f"Frame type error; expected height {self._spec._fmt['height']}, got {frame._fmt['height']}"
|
395
|
+
)
|
396
|
+
if frame._fmt["pix_fmt"] != self._spec._fmt["pix_fmt"]:
|
397
|
+
f_obj = _filter_scale(frame._f, pix_fmt=self._spec._fmt["pix_fmt"])
|
398
|
+
frame = Frame(f_obj, self._spec._fmt)
|
399
|
+
t = self._f_time * self._idx
|
400
|
+
self._frame_buffer.append((t, frame._f if frame is not None else None))
|
401
|
+
self._idx += 1
|
402
|
+
|
403
|
+
if len(self._frame_buffer) >= self._batch_size:
|
404
|
+
self._flush()
|
405
|
+
|
406
|
+
def release(self):
|
407
|
+
if len(self._frame_buffer) > 0:
|
408
|
+
self._flush(True)
|
409
|
+
else:
|
410
|
+
self._explicit_terminate()
|
411
|
+
|
412
|
+
|
413
|
+
class _YrdenVideoWriter:
|
278
414
|
def __init__(self, path, fourcc, fps, size):
|
279
415
|
assert isinstance(fourcc, VideoWriter_fourcc)
|
280
416
|
if path is not None and not isinstance(path, str):
|
@@ -393,13 +529,18 @@ def vidplay(video, *args, **kwargs):
|
|
393
529
|
Args:
|
394
530
|
video: one of [vidformer.Spec, vidformer.Source, vidformer.cv2.VideoWriter]
|
395
531
|
"""
|
396
|
-
|
397
532
|
if isinstance(video, vf.Spec):
|
398
533
|
return video.play(_server(), *args, **kwargs)
|
399
534
|
elif isinstance(video, vf.Source):
|
400
535
|
return video.play(_server(), *args, **kwargs)
|
401
536
|
elif isinstance(video, VideoWriter):
|
537
|
+
return vidplay(video._writer, *args, **kwargs)
|
538
|
+
elif isinstance(video, _YrdenVideoWriter):
|
402
539
|
return video.spec().play(_server(), *args, **kwargs)
|
540
|
+
elif isinstance(video, _IgniVideoWriter):
|
541
|
+
return video._spec.play(*args, **kwargs)
|
542
|
+
elif isinstance(video, igni.IgniSpec):
|
543
|
+
return video.play(*args, **kwargs)
|
403
544
|
else:
|
404
545
|
raise Exception("Unsupported video type to vidplay")
|
405
546
|
|
@@ -0,0 +1 @@
|
|
1
|
+
from .vf_igni import *
|
@@ -0,0 +1,285 @@
|
|
1
|
+
from .. import vf
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from fractions import Fraction
|
5
|
+
from urllib.parse import urlparse
|
6
|
+
|
7
|
+
|
8
|
+
class IgniServer:
|
9
|
+
def __init__(self, endpoint: str, api_key: str):
|
10
|
+
if not endpoint.startswith("http://") and not endpoint.startswith("https://"):
|
11
|
+
raise Exception("Endpoint must start with http:// or https://")
|
12
|
+
if endpoint.endswith("/"):
|
13
|
+
raise Exception("Endpoint must not end with /")
|
14
|
+
self._endpoint = endpoint
|
15
|
+
|
16
|
+
self._api_key = api_key
|
17
|
+
response = requests.get(
|
18
|
+
f"{self._endpoint}/auth",
|
19
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
20
|
+
)
|
21
|
+
if not response.ok:
|
22
|
+
raise Exception(response.text)
|
23
|
+
response = response.json()
|
24
|
+
assert response["status"] == "ok"
|
25
|
+
|
26
|
+
def get_source(self, id: str):
|
27
|
+
assert type(id) == str
|
28
|
+
response = requests.get(
|
29
|
+
f"{self._endpoint}/source/{id}",
|
30
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
31
|
+
)
|
32
|
+
if not response.ok:
|
33
|
+
raise Exception(response.text)
|
34
|
+
response = response.json()
|
35
|
+
return IgniSource(response["id"], response)
|
36
|
+
|
37
|
+
def list_sources(self):
|
38
|
+
response = requests.get(
|
39
|
+
f"{self._endpoint}/source",
|
40
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
41
|
+
)
|
42
|
+
if not response.ok:
|
43
|
+
raise Exception(response.text)
|
44
|
+
response = response.json()
|
45
|
+
return response
|
46
|
+
|
47
|
+
def delete_source(self, id: str):
|
48
|
+
assert type(id) == str
|
49
|
+
response = requests.delete(
|
50
|
+
f"{self._endpoint}/source/{id}",
|
51
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
52
|
+
)
|
53
|
+
if not response.ok:
|
54
|
+
raise Exception(response.text)
|
55
|
+
response = response.json()
|
56
|
+
assert response["status"] == "ok"
|
57
|
+
|
58
|
+
def search_source(self, name, stream_idx, storage_service, storage_config):
|
59
|
+
assert type(name) == str
|
60
|
+
assert type(stream_idx) == int
|
61
|
+
assert type(storage_service) == str
|
62
|
+
assert type(storage_config) == dict
|
63
|
+
for k, v in storage_config.items():
|
64
|
+
assert type(k) == str
|
65
|
+
assert type(v) == str
|
66
|
+
req = {
|
67
|
+
"name": name,
|
68
|
+
"stream_idx": stream_idx,
|
69
|
+
"storage_service": storage_service,
|
70
|
+
"storage_config": storage_config,
|
71
|
+
}
|
72
|
+
response = requests.post(
|
73
|
+
f"{self._endpoint}/source/search",
|
74
|
+
json=req,
|
75
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
76
|
+
)
|
77
|
+
if not response.ok:
|
78
|
+
raise Exception(response.text)
|
79
|
+
response = response.json()
|
80
|
+
return response
|
81
|
+
|
82
|
+
def create_source(self, name, stream_idx, storage_service, storage_config):
|
83
|
+
assert type(name) == str
|
84
|
+
assert type(stream_idx) == int
|
85
|
+
assert type(storage_service) == str
|
86
|
+
assert type(storage_config) == dict
|
87
|
+
for k, v in storage_config.items():
|
88
|
+
assert type(k) == str
|
89
|
+
assert type(v) == str
|
90
|
+
req = {
|
91
|
+
"name": name,
|
92
|
+
"stream_idx": stream_idx,
|
93
|
+
"storage_service": storage_service,
|
94
|
+
"storage_config": storage_config,
|
95
|
+
}
|
96
|
+
response = requests.post(
|
97
|
+
f"{self._endpoint}/source",
|
98
|
+
json=req,
|
99
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
100
|
+
)
|
101
|
+
if not response.ok:
|
102
|
+
raise Exception(response.text)
|
103
|
+
response = response.json()
|
104
|
+
assert response["status"] == "ok"
|
105
|
+
id = response["id"]
|
106
|
+
return self.get_source(id)
|
107
|
+
|
108
|
+
def source(self, name, stream_idx, storage_service, storage_config):
|
109
|
+
"""Convenience function for accessing sources.
|
110
|
+
|
111
|
+
Tries to find a source with the given name, stream_idx, storage_service, and storage_config.
|
112
|
+
If no source is found, creates a new source with the given parameters.
|
113
|
+
"""
|
114
|
+
|
115
|
+
sources = self.search_source(name, stream_idx, storage_service, storage_config)
|
116
|
+
if len(sources) == 0:
|
117
|
+
return self.create_source(name, stream_idx, storage_service, storage_config)
|
118
|
+
return self.get_source(sources[0])
|
119
|
+
|
120
|
+
def get_spec(self, id: str):
|
121
|
+
assert type(id) == str
|
122
|
+
response = requests.get(
|
123
|
+
f"{self._endpoint}/spec/{id}",
|
124
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
125
|
+
)
|
126
|
+
if not response.ok:
|
127
|
+
raise Exception(response.text)
|
128
|
+
response = response.json()
|
129
|
+
return IgniSpec(response["id"], response)
|
130
|
+
|
131
|
+
def list_specs(self):
|
132
|
+
response = requests.get(
|
133
|
+
f"{self._endpoint}/spec",
|
134
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
135
|
+
)
|
136
|
+
if not response.ok:
|
137
|
+
raise Exception(response.text)
|
138
|
+
response = response.json()
|
139
|
+
return response
|
140
|
+
|
141
|
+
def create_spec(
|
142
|
+
self,
|
143
|
+
width,
|
144
|
+
height,
|
145
|
+
pix_fmt,
|
146
|
+
vod_segment_length,
|
147
|
+
frame_rate,
|
148
|
+
ready_hook=None,
|
149
|
+
steer_hook=None,
|
150
|
+
):
|
151
|
+
assert type(width) == int
|
152
|
+
assert type(height) == int
|
153
|
+
assert type(pix_fmt) == str
|
154
|
+
assert type(vod_segment_length) == Fraction
|
155
|
+
assert type(frame_rate) == Fraction
|
156
|
+
assert type(ready_hook) == str or ready_hook is None
|
157
|
+
assert type(steer_hook) == str or steer_hook is None
|
158
|
+
|
159
|
+
req = {
|
160
|
+
"width": width,
|
161
|
+
"height": height,
|
162
|
+
"pix_fmt": pix_fmt,
|
163
|
+
"vod_segment_length": [
|
164
|
+
vod_segment_length.numerator,
|
165
|
+
vod_segment_length.denominator,
|
166
|
+
],
|
167
|
+
"frame_rate": [frame_rate.numerator, frame_rate.denominator],
|
168
|
+
"ready_hook": ready_hook,
|
169
|
+
"steer_hook": steer_hook,
|
170
|
+
}
|
171
|
+
response = requests.post(
|
172
|
+
f"{self._endpoint}/spec",
|
173
|
+
json=req,
|
174
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
175
|
+
)
|
176
|
+
if not response.ok:
|
177
|
+
raise Exception(response.text)
|
178
|
+
response = response.json()
|
179
|
+
assert response["status"] == "ok"
|
180
|
+
return self.get_spec(response["id"])
|
181
|
+
|
182
|
+
def delete_spec(self, id: str):
|
183
|
+
assert type(id) == str
|
184
|
+
response = requests.delete(
|
185
|
+
f"{self._endpoint}/spec/{id}",
|
186
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
187
|
+
)
|
188
|
+
if not response.ok:
|
189
|
+
raise Exception(response.text)
|
190
|
+
response = response.json()
|
191
|
+
assert response["status"] == "ok"
|
192
|
+
|
193
|
+
def push_spec_part(self, spec_id, pos, frames, terminal):
|
194
|
+
if type(spec_id) == IgniSpec:
|
195
|
+
spec_id = spec_id._id
|
196
|
+
assert type(spec_id) == str
|
197
|
+
assert type(pos) == int
|
198
|
+
assert type(frames) == list
|
199
|
+
assert type(terminal) == bool
|
200
|
+
|
201
|
+
req_frames = []
|
202
|
+
for frame in frames:
|
203
|
+
assert type(frame) == tuple
|
204
|
+
assert len(frame) == 2
|
205
|
+
t = frame[0]
|
206
|
+
f = frame[1]
|
207
|
+
assert type(t) == Fraction
|
208
|
+
assert f is None or type(f) == vf.SourceExpr or type(f) == vf.FilterExpr
|
209
|
+
req_frames.append(
|
210
|
+
[
|
211
|
+
[t.numerator, t.denominator],
|
212
|
+
f._to_json_spec() if f is not None else None,
|
213
|
+
]
|
214
|
+
)
|
215
|
+
|
216
|
+
req = {
|
217
|
+
"pos": pos,
|
218
|
+
"frames": req_frames,
|
219
|
+
"terminal": terminal,
|
220
|
+
}
|
221
|
+
response = requests.post(
|
222
|
+
f"{self._endpoint}/spec/{spec_id}/part",
|
223
|
+
json=req,
|
224
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
225
|
+
)
|
226
|
+
if not response.ok:
|
227
|
+
raise Exception(response.text)
|
228
|
+
response = response.json()
|
229
|
+
assert response["status"] == "ok"
|
230
|
+
|
231
|
+
|
232
|
+
class IgniSource:
|
233
|
+
def __init__(self, id, src):
|
234
|
+
self._name = id
|
235
|
+
self._fmt = {
|
236
|
+
"width": src["width"],
|
237
|
+
"height": src["height"],
|
238
|
+
"pix_fmt": src["pix_fmt"],
|
239
|
+
}
|
240
|
+
self._ts = [Fraction(x[0], x[1]) for x in src["ts"]]
|
241
|
+
self.iloc = vf.SourceILoc(self)
|
242
|
+
|
243
|
+
def id(self):
|
244
|
+
return self._name
|
245
|
+
|
246
|
+
def fmt(self):
|
247
|
+
return {**self._fmt}
|
248
|
+
|
249
|
+
def ts(self):
|
250
|
+
return self._ts.copy()
|
251
|
+
|
252
|
+
def __len__(self):
|
253
|
+
return len(self._ts)
|
254
|
+
|
255
|
+
def __getitem__(self, idx):
|
256
|
+
if type(idx) != Fraction:
|
257
|
+
raise Exception("Source index must be a Fraction")
|
258
|
+
return vf.SourceExpr(self, idx, False)
|
259
|
+
|
260
|
+
def __repr__(self):
|
261
|
+
return f"IgniSource({self._name})"
|
262
|
+
|
263
|
+
|
264
|
+
class IgniSpec:
|
265
|
+
def __init__(self, id, src):
|
266
|
+
self._id = id
|
267
|
+
self._fmt = {
|
268
|
+
"width": src["width"],
|
269
|
+
"height": src["height"],
|
270
|
+
"pix_fmt": src["pix_fmt"],
|
271
|
+
}
|
272
|
+
self._vod_endpoint = src["vod_endpoint"]
|
273
|
+
parsed_url = urlparse(self._vod_endpoint)
|
274
|
+
self._hls_js_url = f"{parsed_url.scheme}://{parsed_url.netloc}/hls.js"
|
275
|
+
|
276
|
+
def id(self):
|
277
|
+
return self._id
|
278
|
+
|
279
|
+
def play(self, *args, **kwargs):
|
280
|
+
url = f"{self._vod_endpoint}playlist.m3u8"
|
281
|
+
status_url = f"{self._vod_endpoint}status"
|
282
|
+
hls_js_url = self._hls_js_url
|
283
|
+
return vf._play(
|
284
|
+
self._id, url, hls_js_url, *args, **kwargs, status_url=status_url
|
285
|
+
)
|
vidformer/vf.py
CHANGED
@@ -52,6 +52,134 @@ def _check_hls_link_exists(url, max_attempts=150, delay=0.1):
|
|
52
52
|
return None
|
53
53
|
|
54
54
|
|
55
|
+
def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
|
56
|
+
# The namespace is so multiple videos in one tab don't conflict
|
57
|
+
|
58
|
+
if method == "html":
|
59
|
+
from IPython.display import HTML
|
60
|
+
|
61
|
+
if not status_url:
|
62
|
+
html_code = f"""
|
63
|
+
<!DOCTYPE html>
|
64
|
+
<html>
|
65
|
+
<head>
|
66
|
+
<title>HLS Video Player</title>
|
67
|
+
<!-- Include hls.js library -->
|
68
|
+
<script src="{hls_js_url}"></script>
|
69
|
+
</head>
|
70
|
+
<body>
|
71
|
+
<video id="video-{namespace}" controls width="640" height="360" autoplay></video>
|
72
|
+
<script>
|
73
|
+
var video = document.getElementById('video-{namespace}');
|
74
|
+
var videoSrc = '{hls_video_url}';
|
75
|
+
|
76
|
+
if (Hls.isSupported()) {{
|
77
|
+
var hls = new Hls();
|
78
|
+
hls.loadSource(videoSrc);
|
79
|
+
hls.attachMedia(video);
|
80
|
+
hls.on(Hls.Events.MANIFEST_PARSED, function() {{
|
81
|
+
video.play();
|
82
|
+
}});
|
83
|
+
}} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{
|
84
|
+
video.src = videoSrc;
|
85
|
+
video.addEventListener('loadedmetadata', function() {{
|
86
|
+
video.play();
|
87
|
+
}});
|
88
|
+
}} else {{
|
89
|
+
console.error('This browser does not appear to support HLS.');
|
90
|
+
}}
|
91
|
+
</script>
|
92
|
+
</body>
|
93
|
+
</html>
|
94
|
+
"""
|
95
|
+
return HTML(data=html_code)
|
96
|
+
else:
|
97
|
+
html_code = f"""
|
98
|
+
<!DOCTYPE html>
|
99
|
+
<html>
|
100
|
+
<head>
|
101
|
+
<title>HLS Video Player</title>
|
102
|
+
<script src="{hls_js_url}"></script>
|
103
|
+
</head>
|
104
|
+
<body>
|
105
|
+
<div id="container"></div>
|
106
|
+
<script>
|
107
|
+
var statusUrl = '{status_url}';
|
108
|
+
var videoSrc = '{hls_video_url}';
|
109
|
+
var videoNamespace = '{namespace}';
|
110
|
+
|
111
|
+
function showWaiting() {{
|
112
|
+
document.getElementById('container').textContent = 'Waiting...';
|
113
|
+
pollStatus();
|
114
|
+
}}
|
115
|
+
|
116
|
+
function pollStatus() {{
|
117
|
+
setTimeout(function() {{
|
118
|
+
fetch(statusUrl)
|
119
|
+
.then(r => r.json())
|
120
|
+
.then(res => {{
|
121
|
+
if (res.ready) {{
|
122
|
+
document.getElementById('container').textContent = '';
|
123
|
+
attachHls();
|
124
|
+
}} else {{
|
125
|
+
pollStatus();
|
126
|
+
}}
|
127
|
+
}})
|
128
|
+
.catch(e => {{
|
129
|
+
console.error(e);
|
130
|
+
pollStatus();
|
131
|
+
}});
|
132
|
+
}}, 250);
|
133
|
+
}}
|
134
|
+
|
135
|
+
function attachHls() {{
|
136
|
+
var container = document.getElementById('container');
|
137
|
+
container.textContent = '';
|
138
|
+
var video = document.createElement('video');
|
139
|
+
video.id = 'video-' + videoNamespace;
|
140
|
+
video.controls = true;
|
141
|
+
video.width = 640;
|
142
|
+
video.height = 360;
|
143
|
+
container.appendChild(video);
|
144
|
+
if (Hls.isSupported()) {{
|
145
|
+
var hls = new Hls();
|
146
|
+
hls.loadSource(videoSrc);
|
147
|
+
hls.attachMedia(video);
|
148
|
+
hls.on(Hls.Events.MANIFEST_PARSED, function() {{
|
149
|
+
video.play();
|
150
|
+
}});
|
151
|
+
}} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{
|
152
|
+
video.src = videoSrc;
|
153
|
+
video.addEventListener('loadedmetadata', function() {{
|
154
|
+
video.play();
|
155
|
+
}});
|
156
|
+
}}
|
157
|
+
}}
|
158
|
+
|
159
|
+
fetch(statusUrl)
|
160
|
+
.then(r => r.json())
|
161
|
+
.then(res => {{
|
162
|
+
if (res.ready) {{
|
163
|
+
attachHls();
|
164
|
+
}} else {{
|
165
|
+
showWaiting();
|
166
|
+
}}
|
167
|
+
}})
|
168
|
+
.catch(e => {{
|
169
|
+
console.error(e);
|
170
|
+
showWaiting();
|
171
|
+
}});
|
172
|
+
</script>
|
173
|
+
</body>
|
174
|
+
</html>
|
175
|
+
"""
|
176
|
+
return HTML(data=html_code)
|
177
|
+
elif method == "link":
|
178
|
+
return hls_video_url
|
179
|
+
else:
|
180
|
+
raise ValueError("Invalid method")
|
181
|
+
|
182
|
+
|
55
183
|
class Spec:
|
56
184
|
"""
|
57
185
|
A video transformation specification.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
vidformer/__init__.py,sha256=5Hd99Yj_iTCdwQPWojDy3XTksRGCQW3BNpWHnrX3l2g,113
|
2
|
+
vidformer/vf.py,sha256=Niz3V3tMDL2fWM_xTo3vIn9IFMufbaM8gtIfSR8-FSc,35391
|
3
|
+
vidformer/cv2/__init__.py,sha256=wOjDsYyUKlP_Hye8-tyz-msu9xwaPMpN2sGMu3Lh3-w,22
|
4
|
+
vidformer/cv2/vf_cv2.py,sha256=RzkRQOmKuEuQo_-8NItSx16b3ZgwZ5-lRR_AHWRiZGE,24460
|
5
|
+
vidformer/igni/__init__.py,sha256=a7st8_NVuIu3-weoxE4o8AFtQFyXkkMfUBM_8Lj1bl0,23
|
6
|
+
vidformer/igni/vf_igni.py,sha256=QUVURg6EYq-e5ID5X8jt_fKc0OOGnoWOvl3VRNK-bRo,9249
|
7
|
+
vidformer-0.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
8
|
+
vidformer-0.9.0.dist-info/METADATA,sha256=xnIPh-eICQstIrRUu0efurTRN6p62TycHRiW8eh99Ys,1487
|
9
|
+
vidformer-0.9.0.dist-info/RECORD,,
|
vidformer-0.8.0.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
vidformer/__init__.py,sha256=scMlj2LLMBZXdv9ob_Dhb4ObK7RZLzxVgELyaR650xU,113
|
2
|
-
vidformer/vf.py,sha256=GjgaKnbk3o75lwrXP3Ue0LDyTxSf-l0njL5X-6QYZvs,31496
|
3
|
-
vidformer/cv2/__init__.py,sha256=wOjDsYyUKlP_Hye8-tyz-msu9xwaPMpN2sGMu3Lh3-w,22
|
4
|
-
vidformer/cv2/vf_cv2.py,sha256=8shv614pmNHQyHMqjJSZf08j3eNxE_XtfdLd_kyh5no,19396
|
5
|
-
vidformer-0.8.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
6
|
-
vidformer-0.8.0.dist-info/METADATA,sha256=aXFYRxtNx4hfWlqW7Zx_RW-X2CuN3-adYwRrCUkLtI4,1487
|
7
|
-
vidformer-0.8.0.dist-info/RECORD,,
|
File without changes
|