vidformer 0.7.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  """A Python library for creating and viewing videos with vidformer."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
4
4
 
5
5
  from .vf import *
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
@@ -46,6 +49,9 @@ LINE_8 = 8
46
49
  LINE_AA = 16
47
50
 
48
51
  _inline_mat = vf.Filter("_inline_mat")
52
+ _slice_mat = vf.Filter("_slice_mat")
53
+ _slice_write_mat = vf.Filter("_slice_write_mat")
54
+
49
55
 
50
56
  _filter_scale = vf.Filter("Scale")
51
57
  _filter_rectangle = vf.Filter("cv2.rectangle")
@@ -75,10 +81,10 @@ def _server():
75
81
  return _global_cv2_server
76
82
 
77
83
 
78
- def set_cv2_server(server: vf.YrdenServer):
84
+ def set_cv2_server(server):
79
85
  """Set the server to use for the cv2 frontend."""
80
86
  global _global_cv2_server
81
- assert isinstance(server, vf.YrdenServer)
87
+ assert isinstance(server, vf.YrdenServer) or isinstance(server, igni.IgniServer)
82
88
  _global_cv2_server = server
83
89
 
84
90
 
@@ -119,43 +125,147 @@ class Frame:
119
125
  frame = frame[:, :, ::-1] # convert RGB to BGR
120
126
  return frame
121
127
 
128
+ def __getitem__(self, key):
129
+ if not isinstance(key, tuple):
130
+ raise NotImplementedError("Only 2D slicing is supported")
131
+
132
+ if len(key) != 2:
133
+ raise NotImplementedError("Only 2D slicing is supported")
134
+
135
+ if not all(isinstance(x, slice) for x in key):
136
+ raise NotImplementedError("Only 2D slicing is supported")
137
+
138
+ miny = key[0].start if key[0].start is not None else 0
139
+ maxy = key[0].stop if key[0].stop is not None else self.shape[0]
140
+ minx = key[1].start if key[1].start is not None else 0
141
+ maxx = key[1].stop if key[1].stop is not None else self.shape[1]
142
+
143
+ # handle negative indices
144
+ if miny < 0:
145
+ miny = self.shape[0] + miny
146
+ if maxy < 0:
147
+ maxy = self.shape[0] + maxy
148
+ if minx < 0:
149
+ minx = self.shape[1] + minx
150
+ if maxx < 0:
151
+ maxx = self.shape[1] + maxx
152
+
153
+ if (
154
+ maxy <= miny
155
+ or maxx <= minx
156
+ or miny < 0
157
+ or minx < 0
158
+ or maxy > self.shape[0]
159
+ or maxx > self.shape[1]
160
+ ):
161
+ raise NotImplementedError("Invalid slice")
162
+
163
+ f = _slice_mat(self._f, miny, maxy, minx, maxx)
164
+ fmt = self._fmt.copy()
165
+ fmt["width"] = maxx - minx
166
+ fmt["height"] = maxy - miny
167
+ return Frame(f, fmt)
168
+
169
+ def __setitem__(self, key, value):
170
+ value = frameify(value, "value")
171
+
172
+ if not isinstance(key, tuple):
173
+ raise NotImplementedError("Only 2D slicing is supported")
174
+
175
+ if len(key) != 2:
176
+ raise NotImplementedError("Only 2D slicing is supported")
177
+
178
+ if not all(isinstance(x, slice) for x in key):
179
+ raise NotImplementedError("Only 2D slicing is supported")
180
+
181
+ miny = key[0].start if key[0].start is not None else 0
182
+ maxy = key[0].stop if key[0].stop is not None else self.shape[0]
183
+ minx = key[1].start if key[1].start is not None else 0
184
+ maxx = key[1].stop if key[1].stop is not None else self.shape[1]
185
+
186
+ # handle negative indices
187
+ if miny < 0:
188
+ miny = self.shape[0] + miny
189
+ if maxy < 0:
190
+ maxy = self.shape[0] + maxy
191
+ if minx < 0:
192
+ minx = self.shape[1] + minx
193
+ if maxx < 0:
194
+ maxx = self.shape[1] + maxx
195
+
196
+ if (
197
+ maxy <= miny
198
+ or maxx <= minx
199
+ or miny < 0
200
+ or minx < 0
201
+ or maxy > self.shape[0]
202
+ or maxx > self.shape[1]
203
+ ):
204
+ raise NotImplementedError("Invalid slice")
205
+
206
+ if value.shape[0] != maxy - miny or value.shape[1] != maxx - minx:
207
+ raise NotImplementedError("Shape mismatch")
208
+
209
+ self._mut()
210
+ value._mut()
211
+
212
+ self._f = _slice_write_mat(self._f, value._f, miny, maxy, minx, maxx)
213
+
122
214
 
123
215
  def _inline_frame(arr):
124
- assert arr.dtype == np.uint8
125
- assert arr.ndim == 3
126
- assert arr.shape[2] == 3
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")
127
222
 
128
- # convert BGR to RGB
129
223
  arr = arr[:, :, ::-1]
224
+ if not arr.flags["C_CONTIGUOUS"]:
225
+ arr = np.ascontiguousarray(arr)
130
226
 
131
227
  width = arr.shape[1]
132
228
  height = arr.shape[0]
133
229
  pix_fmt = "rgb24"
134
230
 
135
- f = _inline_mat(arr.tobytes(), width=width, height=height, pix_fmt=pix_fmt)
136
- fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
137
- return Frame(f, fmt)
231
+ data_gzip = zlib.compress(memoryview(arr), level=1)
138
232
 
233
+ f = _inline_mat(
234
+ data_gzip, width=width, height=height, pix_fmt=pix_fmt, compression="zlib"
235
+ )
236
+ fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
139
237
 
140
- def _framify(obj, field_name=None):
141
- if isinstance(obj, Frame):
142
- return obj
143
- elif isinstance(obj, np.ndarray):
144
- return _inline_frame(obj)
145
- else:
146
- if field_name is not None:
147
- raise Exception(
148
- f"Unsupported type for field {field_name}, expected Frame or np.ndarray"
149
- )
150
- else:
151
- raise Exception("Unsupported type, expected Frame or np.ndarray")
238
+ # Return the resulting Frame object
239
+ return Frame(f, fmt)
152
240
 
153
241
 
154
242
  class VideoCapture:
155
243
  def __init__(self, path):
156
- self._path = path
157
244
  server = _server()
158
- self._source = vf.Source(server, str(uuid.uuid4()), path, 0)
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
159
269
  self._next_frame_idx = 0
160
270
 
161
271
  def isOpened(self):
@@ -188,7 +298,7 @@ class VideoCapture:
188
298
  raise Exception(f"Unsupported property {prop}")
189
299
 
190
300
  def read(self):
191
- if self._next_frame_idx >= len(self._source.ts()):
301
+ if self._next_frame_idx >= len(self._source):
192
302
  return False, None
193
303
  frame = self._source.iloc[self._next_frame_idx]
194
304
  self._next_frame_idx += 1
@@ -200,6 +310,107 @@ class VideoCapture:
200
310
 
201
311
 
202
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:
203
414
  def __init__(self, path, fourcc, fps, size):
204
415
  assert isinstance(fourcc, VideoWriter_fourcc)
205
416
  if path is not None and not isinstance(path, str):
@@ -213,7 +424,7 @@ class VideoWriter:
213
424
  self._pix_fmt = "yuv420p"
214
425
 
215
426
  def write(self, frame):
216
- frame = _framify(frame, "frame")
427
+ frame = frameify(frame, "frame")
217
428
 
218
429
  if frame._fmt["pix_fmt"] != self._pix_fmt:
219
430
  f_obj = _filter_scale(frame._f, pix_fmt=self._pix_fmt)
@@ -245,6 +456,24 @@ class VideoWriter_fourcc:
245
456
  self._args = args
246
457
 
247
458
 
459
+ def frameify(obj, field_name=None):
460
+ """
461
+ Turn an object (e.g., ndarray) into a Frame.
462
+ """
463
+
464
+ if isinstance(obj, Frame):
465
+ return obj
466
+ elif isinstance(obj, np.ndarray):
467
+ return _inline_frame(obj)
468
+ else:
469
+ if field_name is not None:
470
+ raise Exception(
471
+ f"Unsupported type for field {field_name}, expected Frame or np.ndarray"
472
+ )
473
+ else:
474
+ raise Exception("Unsupported type, expected Frame or np.ndarray")
475
+
476
+
248
477
  def imread(path, *args):
249
478
  if len(args) > 0:
250
479
  raise NotImplementedError("imread does not support additional arguments")
@@ -260,7 +489,7 @@ def imwrite(path, img, *args):
260
489
  if len(args) > 0:
261
490
  raise NotImplementedError("imwrite does not support additional arguments")
262
491
 
263
- img = _framify(img)
492
+ img = frameify(img)
264
493
 
265
494
  fmt = img._fmt.copy()
266
495
  width = fmt["width"]
@@ -300,13 +529,18 @@ def vidplay(video, *args, **kwargs):
300
529
  Args:
301
530
  video: one of [vidformer.Spec, vidformer.Source, vidformer.cv2.VideoWriter]
302
531
  """
303
-
304
532
  if isinstance(video, vf.Spec):
305
533
  return video.play(_server(), *args, **kwargs)
306
534
  elif isinstance(video, vf.Source):
307
535
  return video.play(_server(), *args, **kwargs)
308
536
  elif isinstance(video, VideoWriter):
537
+ return vidplay(video._writer, *args, **kwargs)
538
+ elif isinstance(video, _YrdenVideoWriter):
309
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)
310
544
  else:
311
545
  raise Exception("Unsupported video type to vidplay")
312
546
 
@@ -316,7 +550,7 @@ def rectangle(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
316
550
  cv.rectangle( img, pt1, pt2, color[, thickness[, lineType[, shift]]] )
317
551
  """
318
552
 
319
- img = _framify(img)
553
+ img = frameify(img)
320
554
  img._mut()
321
555
 
322
556
  assert len(pt1) == 2
@@ -360,7 +594,7 @@ def putText(
360
594
  cv.putText( img, text, org, fontFace, fontScale, color[, thickness[, lineType[, bottomLeftOrigin]]] )
361
595
  """
362
596
 
363
- img = _framify(img)
597
+ img = frameify(img)
364
598
  img._mut()
365
599
 
366
600
  assert isinstance(text, str)
@@ -399,7 +633,7 @@ def arrowedLine(
399
633
  """
400
634
  cv.arrowedLine( img, pt1, pt2, color[, thickness[, line_type[, shift[, tipLength]]]] )
401
635
  """
402
- img = _framify(img)
636
+ img = frameify(img)
403
637
  img._mut()
404
638
 
405
639
  assert len(pt1) == 2
@@ -433,7 +667,7 @@ def arrowedLine(
433
667
 
434
668
 
435
669
  def line(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
436
- img = _framify(img)
670
+ img = frameify(img)
437
671
  img._mut()
438
672
 
439
673
  assert len(pt1) == 2
@@ -463,7 +697,7 @@ def line(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
463
697
 
464
698
 
465
699
  def circle(img, center, radius, color, thickness=None, lineType=None, shift=None):
466
- img = _framify(img)
700
+ img = frameify(img)
467
701
  img._mut()
468
702
 
469
703
  assert len(center) == 2
@@ -514,8 +748,8 @@ def addWeighted(src1, alpha, src2, beta, gamma, dst=None, dtype=-1):
514
748
  """
515
749
  cv.addWeighted( src1, alpha, src2, beta, gamma[, dst[, dtype]] ) -> dst
516
750
  """
517
- src1 = _framify(src1, "src1")
518
- src2 = _framify(src2, "src2")
751
+ src1 = frameify(src1, "src1")
752
+ src2 = frameify(src2, "src2")
519
753
  src1._mut()
520
754
  src2._mut()
521
755
 
@@ -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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: vidformer
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A Python library for creating and viewing videos with vidformer.
5
5
  Author-email: Dominik Winecki <dominikwinecki@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -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,,
@@ -1,7 +0,0 @@
1
- vidformer/__init__.py,sha256=uCe1BJKzIv5LzLK4TZLlOtw6ru8vCUsf7vJkov5-oCE,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=Rta6wCIVaOAnkGXlpqE06qVjFmnn9FnuP7LUD9xrRHU,16413
5
- vidformer-0.7.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
6
- vidformer-0.7.0.dist-info/METADATA,sha256=4YKlG7kViJIWONuWeJEkxdIy2iZg4wZJ4NWzaPWATBw,1487
7
- vidformer-0.7.0.dist-info/RECORD,,