vidformer 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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,,