vidformer 0.11.0__py3-none-any.whl → 1.0.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
@@ -9,27 +9,17 @@ vidformer-py is a Python 🐍 interface for [vidformer](https://github.com/ixlab
9
9
  * [🧑‍💻 Source Code](https://github.com/ixlab/vidformer/tree/main/vidformer-py/)
10
10
  """
11
11
 
12
- __version__ = "0.11.0"
12
+ __version__ = "1.0.0"
13
13
 
14
14
 
15
15
  import base64
16
16
  import gzip
17
17
  import json
18
- import multiprocessing
19
- import os
20
- import random
21
- import re
22
- import socket
23
18
  import struct
24
- import subprocess
25
- import threading
26
19
  import time
27
- import uuid
28
20
  from fractions import Fraction
29
21
  from urllib.parse import urlparse
30
22
 
31
- import msgpack
32
- import numpy as np
33
23
  import requests
34
24
 
35
25
  _in_notebook = False
@@ -105,14 +95,14 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
105
95
  <script src="{hls_js_url}"></script>
106
96
  </head>
107
97
  <body>
108
- <div id="container"></div>
98
+ <div id="container-{namespace}"></div>
109
99
  <script>
110
100
  var statusUrl = '{status_url}';
111
101
  var videoSrc = '{hls_video_url}';
112
102
  var videoNamespace = '{namespace}';
113
103
 
114
104
  function showWaiting() {{
115
- document.getElementById('container').textContent = 'Waiting...';
105
+ document.getElementById('container-{namespace}').textContent = 'Waiting...';
116
106
  pollStatus();
117
107
  }}
118
108
 
@@ -122,7 +112,7 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
122
112
  .then(r => r.json())
123
113
  .then(res => {{
124
114
  if (res.ready) {{
125
- document.getElementById('container').textContent = '';
115
+ document.getElementById('container-{namespace}').textContent = '';
126
116
  attachHls();
127
117
  }} else {{
128
118
  pollStatus();
@@ -136,7 +126,7 @@ def _play(namespace, hls_video_url, hls_js_url, method="html", status_url=None):
136
126
  }}
137
127
 
138
128
  function attachHls() {{
139
- var container = document.getElementById('container');
129
+ var container = document.getElementById('container-{namespace}');
140
130
  container.textContent = '';
141
131
  var video = document.createElement('video');
142
132
  video.id = 'video-' + videoNamespace;
@@ -391,7 +381,7 @@ class _FrameExpressionBlock:
391
381
  }
392
382
 
393
383
 
394
- class IgniSource:
384
+ class Source:
395
385
  def __init__(self, id: str, src):
396
386
  self._name = id
397
387
  self._fmt = {
@@ -420,10 +410,10 @@ class IgniSource:
420
410
  return SourceExpr(self, idx, False)
421
411
 
422
412
  def __repr__(self):
423
- return f"IgniSource({self._name})"
413
+ return f"Source({self._name})"
424
414
 
425
415
 
426
- class IgniSpec:
416
+ class Spec:
427
417
  def __init__(self, id: str, src):
428
418
  self._id = id
429
419
  self._fmt = {
@@ -438,14 +428,14 @@ class IgniSpec:
438
428
  def id(self) -> str:
439
429
  return self._id
440
430
 
441
- def play(self, *args, **kwargs):
431
+ def play(self, method):
442
432
  url = f"{self._vod_endpoint}playlist.m3u8"
443
433
  status_url = f"{self._vod_endpoint}status"
444
434
  hls_js_url = self._hls_js_url
445
- return _play(self._id, url, hls_js_url, *args, **kwargs, status_url=status_url)
435
+ return _play(self._id, url, hls_js_url, method=method, status_url=status_url)
446
436
 
447
437
 
448
- class IgniServer:
438
+ class Server:
449
439
  def __init__(self, endpoint: str, api_key: str):
450
440
  if not endpoint.startswith("http://") and not endpoint.startswith("https://"):
451
441
  raise Exception("Endpoint must start with http:// or https://")
@@ -457,7 +447,7 @@ class IgniServer:
457
447
  self._session = requests.Session()
458
448
  self._session.headers.update({"Authorization": f"Bearer {self._api_key}"})
459
449
  response = self._session.get(
460
- f"{self._endpoint}/auth",
450
+ f"{self._endpoint}/v2/auth",
461
451
  headers={"Authorization": f"Bearer {self._api_key}"},
462
452
  )
463
453
  if not response.ok:
@@ -465,20 +455,20 @@ class IgniServer:
465
455
  response = response.json()
466
456
  assert response["status"] == "ok"
467
457
 
468
- def get_source(self, id: str) -> IgniSource:
458
+ def get_source(self, id: str) -> Source:
469
459
  assert type(id) is str
470
460
  response = self._session.get(
471
- f"{self._endpoint}/source/{id}",
461
+ f"{self._endpoint}/v2/source/{id}",
472
462
  headers={"Authorization": f"Bearer {self._api_key}"},
473
463
  )
474
464
  if not response.ok:
475
465
  raise Exception(response.text)
476
466
  response = response.json()
477
- return IgniSource(response["id"], response)
467
+ return Source(response["id"], response)
478
468
 
479
469
  def list_sources(self) -> list[str]:
480
470
  response = self._session.get(
481
- f"{self._endpoint}/source",
471
+ f"{self._endpoint}/v2/source",
482
472
  headers={"Authorization": f"Bearer {self._api_key}"},
483
473
  )
484
474
  if not response.ok:
@@ -489,7 +479,7 @@ class IgniServer:
489
479
  def delete_source(self, id: str):
490
480
  assert type(id) is str
491
481
  response = self._session.delete(
492
- f"{self._endpoint}/source/{id}",
482
+ f"{self._endpoint}/v2/source/{id}",
493
483
  headers={"Authorization": f"Bearer {self._api_key}"},
494
484
  )
495
485
  if not response.ok:
@@ -514,7 +504,7 @@ class IgniServer:
514
504
  "storage_config": storage_config,
515
505
  }
516
506
  response = self._session.post(
517
- f"{self._endpoint}/source/search",
507
+ f"{self._endpoint}/v2/source/search",
518
508
  json=req,
519
509
  headers={"Authorization": f"Bearer {self._api_key}"},
520
510
  )
@@ -525,7 +515,7 @@ class IgniServer:
525
515
 
526
516
  def create_source(
527
517
  self, name, stream_idx, storage_service, storage_config
528
- ) -> IgniSource:
518
+ ) -> Source:
529
519
  assert type(name) is str
530
520
  assert type(stream_idx) is int
531
521
  assert type(storage_service) is str
@@ -540,7 +530,7 @@ class IgniServer:
540
530
  "storage_config": storage_config,
541
531
  }
542
532
  response = self._session.post(
543
- f"{self._endpoint}/source",
533
+ f"{self._endpoint}/v2/source",
544
534
  json=req,
545
535
  headers={"Authorization": f"Bearer {self._api_key}"},
546
536
  )
@@ -551,7 +541,7 @@ class IgniServer:
551
541
  id = response["id"]
552
542
  return self.get_source(id)
553
543
 
554
- def source(self, name, stream_idx, storage_service, storage_config) -> IgniSource:
544
+ def source(self, name, stream_idx, storage_service, storage_config) -> Source:
555
545
  """Convenience function for accessing sources.
556
546
 
557
547
  Tries to find a source with the given name, stream_idx, storage_service, and storage_config.
@@ -563,20 +553,20 @@ class IgniServer:
563
553
  return self.create_source(name, stream_idx, storage_service, storage_config)
564
554
  return self.get_source(sources[0])
565
555
 
566
- def get_spec(self, id: str) -> IgniSpec:
556
+ def get_spec(self, id: str) -> Spec:
567
557
  assert type(id) is str
568
558
  response = self._session.get(
569
- f"{self._endpoint}/spec/{id}",
559
+ f"{self._endpoint}/v2/spec/{id}",
570
560
  headers={"Authorization": f"Bearer {self._api_key}"},
571
561
  )
572
562
  if not response.ok:
573
563
  raise Exception(response.text)
574
564
  response = response.json()
575
- return IgniSpec(response["id"], response)
565
+ return Spec(response["id"], response)
576
566
 
577
567
  def list_specs(self) -> list[str]:
578
568
  response = self._session.get(
579
- f"{self._endpoint}/spec",
569
+ f"{self._endpoint}/v2/spec",
580
570
  headers={"Authorization": f"Bearer {self._api_key}"},
581
571
  )
582
572
  if not response.ok:
@@ -594,7 +584,7 @@ class IgniServer:
594
584
  ready_hook=None,
595
585
  steer_hook=None,
596
586
  ttl=None,
597
- ) -> IgniSpec:
587
+ ) -> Spec:
598
588
  assert type(width) is int
599
589
  assert type(height) is int
600
590
  assert type(pix_fmt) is str
@@ -618,7 +608,7 @@ class IgniServer:
618
608
  "ttl": ttl,
619
609
  }
620
610
  response = self._session.post(
621
- f"{self._endpoint}/spec",
611
+ f"{self._endpoint}/v2/spec",
622
612
  json=req,
623
613
  headers={"Authorization": f"Bearer {self._api_key}"},
624
614
  )
@@ -631,7 +621,28 @@ class IgniServer:
631
621
  def delete_spec(self, id: str):
632
622
  assert type(id) is str
633
623
  response = self._session.delete(
634
- f"{self._endpoint}/spec/{id}",
624
+ f"{self._endpoint}/v2/spec/{id}",
625
+ headers={"Authorization": f"Bearer {self._api_key}"},
626
+ )
627
+ if not response.ok:
628
+ raise Exception(response.text)
629
+ response = response.json()
630
+ assert response["status"] == "ok"
631
+
632
+ def export_spec(
633
+ self, id: str, path: str, encoder=None, encoder_opts=None, format=None
634
+ ):
635
+ assert type(id) is str
636
+ assert type(path) is str
637
+ req = {
638
+ "path": path,
639
+ "encoder": encoder,
640
+ "encoder_opts": encoder_opts,
641
+ "format": format,
642
+ }
643
+ response = self._session.post(
644
+ f"{self._endpoint}/v2/spec/{id}/export",
645
+ json=req,
635
646
  headers={"Authorization": f"Bearer {self._api_key}"},
636
647
  )
637
648
  if not response.ok:
@@ -640,7 +651,7 @@ class IgniServer:
640
651
  assert response["status"] == "ok"
641
652
 
642
653
  def push_spec_part(self, spec_id, pos, frames, terminal):
643
- if type(spec_id) is IgniSpec:
654
+ if type(spec_id) is Spec:
644
655
  spec_id = spec_id._id
645
656
  assert type(spec_id) is str
646
657
  assert type(pos) is int
@@ -668,7 +679,7 @@ class IgniServer:
668
679
  "terminal": terminal,
669
680
  }
670
681
  response = self._session.post(
671
- f"{self._endpoint}/spec/{spec_id}/part",
682
+ f"{self._endpoint}/v2/spec/{spec_id}/part",
672
683
  json=req,
673
684
  headers={"Authorization": f"Bearer {self._api_key}"},
674
685
  )
@@ -680,7 +691,7 @@ class IgniServer:
680
691
  def push_spec_part_block(
681
692
  self, spec_id: str, pos, blocks, terminal, compression="gzip"
682
693
  ):
683
- if type(spec_id) is IgniSpec:
694
+ if type(spec_id) is Spec:
684
695
  spec_id = spec_id._id
685
696
  assert type(spec_id) is str
686
697
  assert type(pos) is int
@@ -711,7 +722,7 @@ class IgniServer:
711
722
  "blocks": req_blocks,
712
723
  }
713
724
  response = self._session.post(
714
- f"{self._endpoint}/spec/{spec_id}/part_block",
725
+ f"{self._endpoint}/v2/spec/{spec_id}/part_block",
715
726
  json=req,
716
727
  headers={"Authorization": f"Bearer {self._api_key}"},
717
728
  )
@@ -743,7 +754,7 @@ class IgniServer:
743
754
  },
744
755
  }
745
756
  response = self._session.post(
746
- f"{self._endpoint}/frame",
757
+ f"{self._endpoint}/v2/frame",
747
758
  json=req,
748
759
  headers={"Authorization": f"Bearer {self._api_key}"},
749
760
  )
@@ -756,529 +767,6 @@ class IgniServer:
756
767
  return response_body
757
768
 
758
769
 
759
- class YrdenSpec:
760
- """
761
- A video transformation specification.
762
-
763
- See https://ixlab.github.io/vidformer/concepts.html for more information.
764
- """
765
-
766
- def __init__(self, domain: list[Fraction], render, fmt: dict):
767
- self._domain = domain
768
- self._render = render
769
- self._fmt = fmt
770
-
771
- def __repr__(self):
772
- if len(self._domain) <= 20:
773
- lines = []
774
- for i, t in enumerate(self._domain):
775
- frame_expr = self._render(t, i)
776
- lines.append(
777
- f"{t.numerator}/{t.denominator} => {frame_expr}",
778
- )
779
- return "\n".join(lines)
780
- else:
781
- lines = []
782
- for i, t in enumerate(self._domain[:10]):
783
- frame_expr = self._render(t, i)
784
- lines.append(
785
- f"{t.numerator}/{t.denominator} => {frame_expr}",
786
- )
787
- lines.append("...")
788
- for i, t in enumerate(self._domain[-10:]):
789
- frame_expr = self._render(t, i)
790
- lines.append(
791
- f"{t.numerator}/{t.denominator} => {frame_expr}",
792
- )
793
- return "\n".join(lines)
794
-
795
- def _sources(self):
796
- s = set()
797
- for i, t in enumerate(self._domain):
798
- frame_expr = self._render(t, i)
799
- s = s.union(frame_expr._sources())
800
- return s
801
-
802
- def _to_json_spec(self):
803
- frames = []
804
- s = set()
805
- f = {}
806
- for i, t in enumerate(self._domain):
807
- frame_expr = self._render(t, i)
808
- s = s.union(frame_expr._sources())
809
- f = {**f, **frame_expr._filters()}
810
- frame = [[t.numerator, t.denominator], frame_expr._to_json_spec()]
811
- frames.append(frame)
812
- return {"frames": frames}, s, f
813
-
814
- def play(self, server, method="html", verbose=False):
815
- """Play the video live in the notebook."""
816
-
817
- spec, sources, filters = self._to_json_spec()
818
- spec_json_bytes = json.dumps(spec).encode("utf-8")
819
- spec_obj_json_gzip = gzip.compress(spec_json_bytes, compresslevel=1)
820
- spec_obj_json_gzip_b64 = base64.b64encode(spec_obj_json_gzip).decode("utf-8")
821
-
822
- sources = [
823
- {
824
- "name": s._name,
825
- "path": s._path,
826
- "stream": s._stream,
827
- "service": s._service.as_json() if s._service is not None else None,
828
- }
829
- for s in sources
830
- ]
831
- filters = {
832
- k: {
833
- "filter": v._func,
834
- "args": v._kwargs,
835
- }
836
- for k, v in filters.items()
837
- }
838
-
839
- if verbose:
840
- print(f"Sending to server. Spec is {len(spec_obj_json_gzip_b64)} bytes")
841
-
842
- resp = server._new(spec_obj_json_gzip_b64, sources, filters, self._fmt)
843
- hls_video_url = resp["stream_url"]
844
- hls_player_url = resp["player_url"]
845
- namespace = resp["namespace"]
846
- hls_js_url = server.hls_js_url()
847
-
848
- if method == "link":
849
- return hls_video_url
850
- if method == "player":
851
- return hls_player_url
852
- if method == "iframe":
853
- from IPython.display import IFrame
854
-
855
- return IFrame(hls_player_url, width=1280, height=720)
856
- if method == "html":
857
- from IPython.display import HTML
858
-
859
- # We add a namespace to the video element to avoid conflicts with other videos
860
- html_code = f"""
861
- <!DOCTYPE html>
862
- <html>
863
- <head>
864
- <title>HLS Video Player</title>
865
- <!-- Include hls.js library -->
866
- <script src="{hls_js_url}"></script>
867
- </head>
868
- <body>
869
- <!-- Video element -->
870
- <video id="video-{namespace}" controls width="640" height="360" autoplay></video>
871
- <script>
872
- var video = document.getElementById('video-{namespace}');
873
- var videoSrc = '{hls_video_url}';
874
- var hls = new Hls();
875
- hls.loadSource(videoSrc);
876
- hls.attachMedia(video);
877
- hls.on(Hls.Events.MANIFEST_PARSED, function() {{
878
- video.play();
879
- }});
880
- </script>
881
- </body>
882
- </html>
883
- """
884
- return HTML(data=html_code)
885
- else:
886
- return hls_player_url
887
-
888
- def load(self, server):
889
- spec, sources, filters = self._to_json_spec()
890
- spec_json_bytes = json.dumps(spec).encode("utf-8")
891
- spec_obj_json_gzip = gzip.compress(spec_json_bytes, compresslevel=1)
892
- spec_obj_json_gzip_b64 = base64.b64encode(spec_obj_json_gzip).decode("utf-8")
893
-
894
- sources = [
895
- {
896
- "name": s._name,
897
- "path": s._path,
898
- "stream": s._stream,
899
- "service": s._service.as_json() if s._service is not None else None,
900
- }
901
- for s in sources
902
- ]
903
- filters = {
904
- k: {
905
- "filter": v._func,
906
- "args": v._kwargs,
907
- }
908
- for k, v in filters.items()
909
- }
910
- resp = server._new(spec_obj_json_gzip_b64, sources, filters, self._fmt)
911
- namespace = resp["namespace"]
912
- return YrdenLoader(server, namespace, self._domain)
913
-
914
- def save(self, server, pth, encoder=None, encoder_opts=None, format=None):
915
- """Save the video to a file."""
916
-
917
- assert encoder is None or type(encoder) is str
918
- assert encoder_opts is None or type(encoder_opts) is dict
919
- if encoder_opts is not None:
920
- for k, v in encoder_opts.items():
921
- assert type(k) is str and type(v) is str
922
-
923
- spec, sources, filters = self._to_json_spec()
924
- spec_json_bytes = json.dumps(spec).encode("utf-8")
925
- spec_obj_json_gzip = gzip.compress(spec_json_bytes, compresslevel=1)
926
- spec_obj_json_gzip_b64 = base64.b64encode(spec_obj_json_gzip).decode("utf-8")
927
-
928
- sources = [
929
- {
930
- "name": s._name,
931
- "path": s._path,
932
- "stream": s._stream,
933
- "service": s._service.as_json() if s._service is not None else None,
934
- }
935
- for s in sources
936
- ]
937
- filters = {
938
- k: {
939
- "filter": v._func,
940
- "args": v._kwargs,
941
- }
942
- for k, v in filters.items()
943
- }
944
-
945
- resp = server._export(
946
- pth,
947
- spec_obj_json_gzip_b64,
948
- sources,
949
- filters,
950
- self._fmt,
951
- encoder,
952
- encoder_opts,
953
- format,
954
- )
955
-
956
- return resp
957
-
958
- def _vrod_bench(self, server):
959
- out = {}
960
- pth = "spec.json"
961
- start_t = time.time()
962
- with open(pth, "w") as outfile:
963
- spec, sources, filters = self._to_json_spec()
964
- outfile.write(json.dumps(spec))
965
-
966
- sources = [
967
- {
968
- "name": s._name,
969
- "path": s._path,
970
- "stream": s._stream,
971
- "service": s._service.as_json() if s._service is not None else None,
972
- }
973
- for s in sources
974
- ]
975
- filters = {
976
- k: {
977
- "filter": v._func,
978
- "args": v._kwargs,
979
- }
980
- for k, v in filters.items()
981
- }
982
- end_t = time.time()
983
- out["vrod_create_spec"] = end_t - start_t
984
-
985
- start = time.time()
986
- resp = server._new(pth, sources, filters, self._fmt)
987
- end = time.time()
988
- out["vrod_register"] = end - start
989
-
990
- stream_url = resp["stream_url"]
991
- first_segment = stream_url.replace("stream.m3u8", "segment-0.ts")
992
-
993
- start = time.time()
994
- r = requests.get(first_segment)
995
- r.raise_for_status()
996
- end = time.time()
997
- out["vrod_first_segment"] = end - start
998
- return out
999
-
1000
- def _dve2_bench(self, server):
1001
- pth = "spec.json"
1002
- out = {}
1003
- start_t = time.time()
1004
- with open(pth, "w") as outfile:
1005
- spec, sources, filters = self._to_json_spec()
1006
- outfile.write(json.dumps(spec))
1007
-
1008
- sources = [
1009
- {
1010
- "name": s._name,
1011
- "path": s._path,
1012
- "stream": s._stream,
1013
- "service": s._service.as_json() if s._service is not None else None,
1014
- }
1015
- for s in sources
1016
- ]
1017
- filters = {
1018
- k: {
1019
- "filter": v._func,
1020
- "args": v._kwargs,
1021
- }
1022
- for k, v in filters.items()
1023
- }
1024
- end_t = time.time()
1025
- out["dve2_create_spec"] = end_t - start_t
1026
-
1027
- start = time.time()
1028
- resp = server._export(pth, sources, filters, self._fmt, None, None)
1029
- resp.raise_for_status()
1030
- end = time.time()
1031
- out["dve2_exec"] = end - start
1032
- return out
1033
-
1034
-
1035
- class YrdenLoader:
1036
- def __init__(self, server, namespace: str, domain):
1037
- self._server = server
1038
- self._namespace = namespace
1039
- self._domain = domain
1040
-
1041
- def _chunk(self, start_i, end_i):
1042
- return self._server._raw(self._namespace, start_i, end_i)
1043
-
1044
- def __len__(self):
1045
- return len(self._domain)
1046
-
1047
- def _find_index_by_rational(self, value):
1048
- if value not in self._domain:
1049
- raise ValueError(f"Rational timestamp {value} is not in the domain")
1050
- return self._domain.index(value)
1051
-
1052
- def __getitem__(self, index):
1053
- if isinstance(index, slice):
1054
- start = index.start if index.start is not None else 0
1055
- end = index.stop if index.stop is not None else len(self._domain)
1056
- assert start >= 0 and start < len(self._domain)
1057
- assert end >= 0 and end <= len(self._domain)
1058
- assert start <= end
1059
- num_frames = end - start
1060
- all_bytes = self._chunk(start, end - 1)
1061
- all_bytes_len = len(all_bytes)
1062
- assert all_bytes_len % num_frames == 0
1063
- return [
1064
- all_bytes[
1065
- i
1066
- * all_bytes_len
1067
- // num_frames : (i + 1)
1068
- * all_bytes_len
1069
- // num_frames
1070
- ]
1071
- for i in range(num_frames)
1072
- ]
1073
- elif isinstance(index, int):
1074
- assert index >= 0 and index < len(self._domain)
1075
- return self._chunk(index, index)
1076
- else:
1077
- raise TypeError(
1078
- "Invalid argument type for iloc. Use a slice or an integer."
1079
- )
1080
-
1081
-
1082
- class YrdenServer:
1083
- """
1084
- A connection to a Yrden server.
1085
-
1086
- A yrden server is the main API for local use of vidformer.
1087
- """
1088
-
1089
- def __init__(self, domain=None, port=None, bin=None, hls_prefix=None):
1090
- """
1091
- Connect to a Yrden server
1092
-
1093
- Can either connect to an existing server, if domain and port are provided, or start a new server using the provided binary.
1094
- If no domain or binary is provided, the `VIDFORMER_BIN` environment variable is used.
1095
- """
1096
-
1097
- self._domain = domain
1098
- self._port = port
1099
- self._proc = None
1100
- if self._port is None:
1101
- if bin is None:
1102
- if os.getenv("VIDFORMER_BIN") is not None:
1103
- bin = os.getenv("VIDFORMER_BIN")
1104
- else:
1105
- bin = "vidformer-cli"
1106
-
1107
- self._domain = "localhost"
1108
- self._port = random.randint(49152, 65535)
1109
- cmd = [bin, "yrden", "--port", str(self._port)]
1110
- if _in_notebook:
1111
- # We need to print the URL in the notebook
1112
- # This is a trick to get VS Code to forward the port
1113
- cmd += ["--print-url"]
1114
-
1115
- if hls_prefix is not None:
1116
- if type(hls_prefix) is not str:
1117
- raise Exception("hls_prefix must be a string")
1118
- cmd += ["--hls-prefix", hls_prefix]
1119
-
1120
- self._proc = subprocess.Popen(cmd)
1121
-
1122
- version = _wait_for_url(f"http://{self._domain}:{self._port}/")
1123
- if version is None:
1124
- raise Exception("Failed to connect to server")
1125
-
1126
- expected_version = f"vidformer-yrden v{__version__}"
1127
- if version != expected_version:
1128
- print(
1129
- f"Warning: Expected version `{expected_version}`, got `{version}`. API may not be compatible!"
1130
- )
1131
-
1132
- def _source(self, name: str, path: str, stream: int, service):
1133
- r = requests.post(
1134
- f"http://{self._domain}:{self._port}/source",
1135
- json={
1136
- "name": name,
1137
- "path": path,
1138
- "stream": stream,
1139
- "service": service.as_json() if service is not None else None,
1140
- },
1141
- )
1142
- if not r.ok:
1143
- raise Exception(r.text)
1144
-
1145
- resp = r.json()
1146
- resp["ts"] = [Fraction(x[0], x[1]) for x in resp["ts"]]
1147
- return resp
1148
-
1149
- def _new(self, spec, sources, filters, fmt):
1150
- req = {
1151
- "spec": spec,
1152
- "sources": sources,
1153
- "filters": filters,
1154
- "width": fmt["width"],
1155
- "height": fmt["height"],
1156
- "pix_fmt": fmt["pix_fmt"],
1157
- }
1158
-
1159
- r = requests.post(f"http://{self._domain}:{self._port}/new", json=req)
1160
- if not r.ok:
1161
- raise Exception(r.text)
1162
-
1163
- return r.json()
1164
-
1165
- def _export(self, pth, spec, sources, filters, fmt, encoder, encoder_opts, format):
1166
- req = {
1167
- "spec": spec,
1168
- "sources": sources,
1169
- "filters": filters,
1170
- "width": fmt["width"],
1171
- "height": fmt["height"],
1172
- "pix_fmt": fmt["pix_fmt"],
1173
- "output_path": pth,
1174
- "encoder": encoder,
1175
- "encoder_opts": encoder_opts,
1176
- "format": format,
1177
- }
1178
-
1179
- r = requests.post(f"http://{self._domain}:{self._port}/export", json=req)
1180
- if not r.ok:
1181
- raise Exception(r.text)
1182
-
1183
- return r.json()
1184
-
1185
- def _raw(self, namespace, start_i, end_i):
1186
- r = requests.get(
1187
- f"http://{self._domain}:{self._port}/{namespace}/raw/{start_i}-{end_i}"
1188
- )
1189
- if not r.ok:
1190
- raise Exception(r.text)
1191
- return r.content
1192
-
1193
- def hls_js_url(self):
1194
- """Return the link to the yrden-hosted hls.js file"""
1195
- return f"http://{self._domain}:{self._port}/hls.js"
1196
-
1197
- def __del__(self):
1198
- if self._proc is not None:
1199
- self._proc.kill()
1200
-
1201
-
1202
- class YrdenSource:
1203
- """A video source."""
1204
-
1205
- def __init__(
1206
- self, server: YrdenServer, name: str, path: str, stream: int, service=None
1207
- ):
1208
- if service is None:
1209
- # check if path is a http URL and, if so, automatically set the service
1210
- # for example, the following code should work with just vf.Source(server, "tos_720p", "https://f.dominik.win/data/dve2/tos_720p.mp4")
1211
- # this creates a storage service with endpoint "https://f.dominik.win/" and path "data/dve2/tos_720p.mp4"
1212
- # don't use the root parameter in this case
1213
-
1214
- match = re.match(r"(http|https)://([^/]+)(.*)", path)
1215
- if match is not None:
1216
- endpoint = f"{match.group(1)}://{match.group(2)}"
1217
- path = match.group(3)
1218
- # remove leading slash
1219
- if path.startswith("/"):
1220
- path = path[1:]
1221
- service = YrdenStorageService("http", endpoint=endpoint)
1222
-
1223
- self._server = server
1224
- self._name = name
1225
- self._path = path
1226
- self._stream = stream
1227
- self._service = service
1228
-
1229
- self.iloc = _SourceILoc(self)
1230
-
1231
- self._src = self._server._source(
1232
- self._name, self._path, self._stream, self._service
1233
- )
1234
-
1235
- def fmt(self):
1236
- return {
1237
- "width": self._src["width"],
1238
- "height": self._src["height"],
1239
- "pix_fmt": self._src["pix_fmt"],
1240
- }
1241
-
1242
- def ts(self):
1243
- return self._src["ts"]
1244
-
1245
- def __len__(self):
1246
- return len(self._src["ts"])
1247
-
1248
- def __getitem__(self, idx):
1249
- if type(idx) is not Fraction:
1250
- raise Exception("Source index must be a Fraction")
1251
- return SourceExpr(self, idx, False)
1252
-
1253
- def play(self, *args, **kwargs):
1254
- """Play the video live in the notebook."""
1255
-
1256
- domain = self.ts()
1257
-
1258
- def render(t, _i):
1259
- return self[t]
1260
-
1261
- spec = YrdenSpec(domain, render, self.fmt())
1262
- return spec.play(*args, **kwargs)
1263
-
1264
-
1265
- class YrdenStorageService:
1266
- def __init__(self, service: str, **kwargs):
1267
- if type(service) is not str:
1268
- raise Exception("Service name must be a string")
1269
- self._service = service
1270
- for k, v in kwargs.items():
1271
- if type(v) is not str:
1272
- raise Exception(f"Value of {k} must be a string")
1273
- self._config = kwargs
1274
-
1275
- def as_json(self):
1276
- return {"service": self._service, "config": self._config}
1277
-
1278
- def __repr__(self):
1279
- return f"{self._service}(config={self._config})"
1280
-
1281
-
1282
770
  class SourceExpr:
1283
771
  def __init__(self, source, idx, is_iloc):
1284
772
  self._source = source
@@ -1359,20 +847,8 @@ def _json_arg(arg, skip_data_anot=False):
1359
847
  class Filter:
1360
848
  """A video filter."""
1361
849
 
1362
- def __init__(self, name: str, tl_func=None, **kwargs):
1363
- self._name = name
1364
-
1365
- # tl_func is the top level func, which is the true implementation, not just a pretty name
1366
- if tl_func is None:
1367
- self._func = name
1368
- else:
1369
- self._func = tl_func
1370
-
1371
- # filter infra args, not invocation args
1372
- for k, v in kwargs.items():
1373
- if type(v) is not str:
1374
- raise Exception(f"Value of {k} must be a string")
1375
- self._kwargs = kwargs
850
+ def __init__(self, func: str):
851
+ self._func = func
1376
852
 
1377
853
  def __call__(self, *args, **kwargs):
1378
854
  return FilterExpr(self, args, kwargs)
@@ -1422,259 +898,3 @@ class FilterExpr:
1422
898
  if type(arg) is FilterExpr:
1423
899
  f = {**f, **arg._filters()}
1424
900
  return f
1425
-
1426
-
1427
- class UDF:
1428
- """User-defined filter superclass"""
1429
-
1430
- def __init__(self, name: str):
1431
- self._name = name
1432
- self._socket_path = None
1433
- self._p = None
1434
-
1435
- def filter(self, *args, **kwargs):
1436
- raise Exception("User must implement the filter method")
1437
-
1438
- def filter_type(self, *args, **kwargs):
1439
- raise Exception("User must implement the filter_type method")
1440
-
1441
- def into_filter(self):
1442
- assert self._socket_path is None
1443
- self._socket_path = f"/tmp/vidformer-{self._name}-{str(uuid.uuid4())}.sock"
1444
- self._p = multiprocessing.Process(
1445
- target=_run_udf_host, args=(self, self._socket_path)
1446
- )
1447
- self._p.start()
1448
- return Filter(
1449
- name=self._name, tl_func="IPC", socket=self._socket_path, func=self._name
1450
- )
1451
-
1452
- def _handle_connection(self, connection):
1453
- try:
1454
- while True:
1455
- frame_len = connection.recv(4)
1456
- if not frame_len or len(frame_len) != 4:
1457
- break
1458
- frame_len = int.from_bytes(frame_len, byteorder="big")
1459
- data = connection.recv(frame_len)
1460
- if not data:
1461
- break
1462
-
1463
- while len(data) < frame_len:
1464
- new_data = connection.recv(frame_len - len(data))
1465
- if not new_data:
1466
- raise Exception("Partial data received")
1467
- data += new_data
1468
-
1469
- obj = msgpack.unpackb(data, raw=False)
1470
- f_op, f_args, f_kwargs = (
1471
- obj["op"],
1472
- obj["args"],
1473
- obj["kwargs"],
1474
- )
1475
-
1476
- response = None
1477
- if f_op == "filter":
1478
- f_args = [self._deser_filter(x) for x in f_args]
1479
- f_kwargs = {k: self._deser_filter(v) for k, v in f_kwargs}
1480
- response = self.filter(*f_args, **f_kwargs)
1481
- if type(response) is not UDFFrame:
1482
- raise Exception(
1483
- f"filter must return a UDFFrame, got {type(response)}"
1484
- )
1485
- if response.frame_type().pix_fmt() != "rgb24":
1486
- raise Exception(
1487
- f"filter must return a frame with pix_fmt 'rgb24', got {response.frame_type().pix_fmt()}"
1488
- )
1489
-
1490
- response = response._response_ser()
1491
- elif f_op == "filter_type":
1492
- f_args = [self._deser_filter_type(x) for x in f_args]
1493
- f_kwargs = {k: self._deser_filter_type(v) for k, v in f_kwargs}
1494
- response = self.filter_type(*f_args, **f_kwargs)
1495
- if type(response) is not UDFFrameType:
1496
- raise Exception(
1497
- f"filter_type must return a UDFFrameType, got {type(response)}"
1498
- )
1499
- if response.pix_fmt() != "rgb24":
1500
- raise Exception(
1501
- f"filter_type must return a frame with pix_fmt 'rgb24', got {response.pix_fmt()}"
1502
- )
1503
- response = response._response_ser()
1504
- else:
1505
- raise Exception(f"Unknown operation: {f_op}")
1506
-
1507
- response = msgpack.packb(response, use_bin_type=True)
1508
- response_len = len(response).to_bytes(4, byteorder="big")
1509
- connection.sendall(response_len)
1510
- connection.sendall(response)
1511
- finally:
1512
- connection.close()
1513
-
1514
- def _deser_filter_type(self, obj):
1515
- assert type(obj) is dict
1516
- keys = list(obj.keys())
1517
- assert len(keys) == 1
1518
- type_key = keys[0]
1519
- assert type_key in ["FrameType", "String", "Int", "Bool"]
1520
-
1521
- if type_key == "FrameType":
1522
- frame = obj[type_key]
1523
- assert type(frame) is dict
1524
- assert "width" in frame
1525
- assert "height" in frame
1526
- assert "format" in frame
1527
- assert type(frame["width"]) is int
1528
- assert type(frame["height"]) is int
1529
- assert frame["format"] == 2 # AV_PIX_FMT_RGB24
1530
- return UDFFrameType(frame["width"], frame["height"], "rgb24")
1531
- elif type_key == "String":
1532
- assert type(obj[type_key]) is str
1533
- return obj[type_key]
1534
- elif type_key == "Int":
1535
- assert type(obj[type_key]) is int
1536
- return obj[type_key]
1537
- elif type_key == "Bool":
1538
- assert type(obj[type_key]) is bool
1539
- return obj[type_key]
1540
- else:
1541
- assert False, f"Unknown type: {type_key}"
1542
-
1543
- def _deser_filter(self, obj):
1544
- assert type(obj) is dict
1545
- keys = list(obj.keys())
1546
- assert len(keys) == 1
1547
- type_key = keys[0]
1548
- assert type_key in ["Frame", "String", "Int", "Bool"]
1549
-
1550
- if type_key == "Frame":
1551
- frame = obj[type_key]
1552
- assert type(frame) is dict
1553
- assert "data" in frame
1554
- assert "width" in frame
1555
- assert "height" in frame
1556
- assert "format" in frame
1557
- assert type(frame["width"]) is int
1558
- assert type(frame["height"]) is int
1559
- assert frame["format"] == "rgb24"
1560
- assert type(frame["data"]) is bytes
1561
-
1562
- data = np.frombuffer(frame["data"], dtype=np.uint8)
1563
- data = data.reshape(frame["height"], frame["width"], 3)
1564
- return UDFFrame(
1565
- data, UDFFrameType(frame["width"], frame["height"], "rgb24")
1566
- )
1567
- elif type_key == "String":
1568
- assert type(obj[type_key]) is str
1569
- return obj[type_key]
1570
- elif type_key == "Int":
1571
- assert type(obj[type_key]) is int
1572
- return obj[type_key]
1573
- elif type_key == "Bool":
1574
- assert type(obj[type_key]) is bool
1575
- return obj[type_key]
1576
- else:
1577
- assert False, f"Unknown type: {type_key}"
1578
-
1579
- def _host(self, socket_path: str):
1580
- if os.path.exists(socket_path):
1581
- os.remove(socket_path)
1582
-
1583
- # start listening on the socket
1584
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1585
- sock.bind(socket_path)
1586
- sock.listen(1)
1587
-
1588
- while True:
1589
- # accept incoming connection
1590
- connection, client_address = sock.accept()
1591
- thread = threading.Thread(
1592
- target=self._handle_connection, args=(connection,)
1593
- )
1594
- thread.start()
1595
-
1596
- def __del__(self):
1597
- if self._socket_path is not None:
1598
- self._p.terminate()
1599
- if os.path.exists(self._socket_path):
1600
- # it's possible the process hasn't even created the socket yet
1601
- os.remove(self._socket_path)
1602
-
1603
-
1604
- class UDFFrameType:
1605
- """
1606
- Frame type for use in UDFs.
1607
- """
1608
-
1609
- def __init__(self, width: int, height: int, pix_fmt: str):
1610
- assert type(width) is int
1611
- assert type(height) is int
1612
- assert type(pix_fmt) is str
1613
-
1614
- self._width = width
1615
- self._height = height
1616
- self._pix_fmt = pix_fmt
1617
-
1618
- def width(self):
1619
- return self._width
1620
-
1621
- def height(self):
1622
- return self._height
1623
-
1624
- def pix_fmt(self):
1625
- return self._pix_fmt
1626
-
1627
- def _response_ser(self):
1628
- return {
1629
- "frame_type": {
1630
- "width": self._width,
1631
- "height": self._height,
1632
- "format": 2, # AV_PIX_FMT_RGB24
1633
- }
1634
- }
1635
-
1636
- def __repr__(self):
1637
- return f"FrameType<{self._width}x{self._height}, {self._pix_fmt}>"
1638
-
1639
-
1640
- class UDFFrame:
1641
- """A symbolic reference to a frame for use in UDFs."""
1642
-
1643
- def __init__(self, data: np.ndarray, f_type: UDFFrameType):
1644
- assert type(data) is np.ndarray
1645
- assert type(f_type) is UDFFrameType
1646
-
1647
- # We only support RGB24 for now
1648
- assert data.dtype == np.uint8
1649
- assert data.shape[2] == 3
1650
-
1651
- # check type matches
1652
- assert data.shape[0] == f_type.height()
1653
- assert data.shape[1] == f_type.width()
1654
- assert f_type.pix_fmt() == "rgb24"
1655
-
1656
- self._data = data
1657
- self._f_type = f_type
1658
-
1659
- def data(self):
1660
- return self._data
1661
-
1662
- def frame_type(self):
1663
- return self._f_type
1664
-
1665
- def _response_ser(self):
1666
- return {
1667
- "frame": {
1668
- "data": self._data.tobytes(),
1669
- "width": self._f_type.width(),
1670
- "height": self._f_type.height(),
1671
- "format": "rgb24",
1672
- }
1673
- }
1674
-
1675
- def __repr__(self):
1676
- return f"Frame<{self._f_type.width()}x{self._f_type.height()}, {self._f_type.pix_fmt()}>"
1677
-
1678
-
1679
- def _run_udf_host(udf: UDF, socket_path: str):
1680
- udf._host(socket_path)