vidformer 0.12.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.12.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
@@ -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,534 +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.terminate()
1200
- try:
1201
- self._proc.wait(timeout=1)
1202
- except subprocess.TimeoutExpired:
1203
- self._proc.kill()
1204
- self._proc.wait()
1205
-
1206
-
1207
- class YrdenSource:
1208
- """A video source."""
1209
-
1210
- def __init__(
1211
- self, server: YrdenServer, name: str, path: str, stream: int, service=None
1212
- ):
1213
- if service is None:
1214
- # check if path is a http URL and, if so, automatically set the service
1215
- # for example, the following code should work with just vf.Source(server, "tos_720p", "https://f.dominik.win/data/dve2/tos_720p.mp4")
1216
- # this creates a storage service with endpoint "https://f.dominik.win/" and path "data/dve2/tos_720p.mp4"
1217
- # don't use the root parameter in this case
1218
-
1219
- match = re.match(r"(http|https)://([^/]+)(.*)", path)
1220
- if match is not None:
1221
- endpoint = f"{match.group(1)}://{match.group(2)}"
1222
- path = match.group(3)
1223
- # remove leading slash
1224
- if path.startswith("/"):
1225
- path = path[1:]
1226
- service = YrdenStorageService("http", endpoint=endpoint)
1227
-
1228
- self._server = server
1229
- self._name = name
1230
- self._path = path
1231
- self._stream = stream
1232
- self._service = service
1233
-
1234
- self.iloc = _SourceILoc(self)
1235
-
1236
- self._src = self._server._source(
1237
- self._name, self._path, self._stream, self._service
1238
- )
1239
-
1240
- def fmt(self):
1241
- return {
1242
- "width": self._src["width"],
1243
- "height": self._src["height"],
1244
- "pix_fmt": self._src["pix_fmt"],
1245
- }
1246
-
1247
- def ts(self):
1248
- return self._src["ts"]
1249
-
1250
- def __len__(self):
1251
- return len(self._src["ts"])
1252
-
1253
- def __getitem__(self, idx):
1254
- if type(idx) is not Fraction:
1255
- raise Exception("Source index must be a Fraction")
1256
- return SourceExpr(self, idx, False)
1257
-
1258
- def play(self, *args, **kwargs):
1259
- """Play the video live in the notebook."""
1260
-
1261
- domain = self.ts()
1262
-
1263
- def render(t, _i):
1264
- return self[t]
1265
-
1266
- spec = YrdenSpec(domain, render, self.fmt())
1267
- return spec.play(*args, **kwargs)
1268
-
1269
-
1270
- class YrdenStorageService:
1271
- def __init__(self, service: str, **kwargs):
1272
- if type(service) is not str:
1273
- raise Exception("Service name must be a string")
1274
- self._service = service
1275
- for k, v in kwargs.items():
1276
- if type(v) is not str:
1277
- raise Exception(f"Value of {k} must be a string")
1278
- self._config = kwargs
1279
-
1280
- def as_json(self):
1281
- return {"service": self._service, "config": self._config}
1282
-
1283
- def __repr__(self):
1284
- return f"{self._service}(config={self._config})"
1285
-
1286
-
1287
770
  class SourceExpr:
1288
771
  def __init__(self, source, idx, is_iloc):
1289
772
  self._source = source
@@ -1364,20 +847,8 @@ def _json_arg(arg, skip_data_anot=False):
1364
847
  class Filter:
1365
848
  """A video filter."""
1366
849
 
1367
- def __init__(self, name: str, tl_func=None, **kwargs):
1368
- self._name = name
1369
-
1370
- # tl_func is the top level func, which is the true implementation, not just a pretty name
1371
- if tl_func is None:
1372
- self._func = name
1373
- else:
1374
- self._func = tl_func
1375
-
1376
- # filter infra args, not invocation args
1377
- for k, v in kwargs.items():
1378
- if type(v) is not str:
1379
- raise Exception(f"Value of {k} must be a string")
1380
- self._kwargs = kwargs
850
+ def __init__(self, func: str):
851
+ self._func = func
1381
852
 
1382
853
  def __call__(self, *args, **kwargs):
1383
854
  return FilterExpr(self, args, kwargs)
@@ -1427,259 +898,3 @@ class FilterExpr:
1427
898
  if type(arg) is FilterExpr:
1428
899
  f = {**f, **arg._filters()}
1429
900
  return f
1430
-
1431
-
1432
- class UDF:
1433
- """User-defined filter superclass"""
1434
-
1435
- def __init__(self, name: str):
1436
- self._name = name
1437
- self._socket_path = None
1438
- self._p = None
1439
-
1440
- def filter(self, *args, **kwargs):
1441
- raise Exception("User must implement the filter method")
1442
-
1443
- def filter_type(self, *args, **kwargs):
1444
- raise Exception("User must implement the filter_type method")
1445
-
1446
- def into_filter(self):
1447
- assert self._socket_path is None
1448
- self._socket_path = f"/tmp/vidformer-{self._name}-{str(uuid.uuid4())}.sock"
1449
- self._p = multiprocessing.Process(
1450
- target=_run_udf_host, args=(self, self._socket_path)
1451
- )
1452
- self._p.start()
1453
- return Filter(
1454
- name=self._name, tl_func="IPC", socket=self._socket_path, func=self._name
1455
- )
1456
-
1457
- def _handle_connection(self, connection):
1458
- try:
1459
- while True:
1460
- frame_len = connection.recv(4)
1461
- if not frame_len or len(frame_len) != 4:
1462
- break
1463
- frame_len = int.from_bytes(frame_len, byteorder="big")
1464
- data = connection.recv(frame_len)
1465
- if not data:
1466
- break
1467
-
1468
- while len(data) < frame_len:
1469
- new_data = connection.recv(frame_len - len(data))
1470
- if not new_data:
1471
- raise Exception("Partial data received")
1472
- data += new_data
1473
-
1474
- obj = msgpack.unpackb(data, raw=False)
1475
- f_op, f_args, f_kwargs = (
1476
- obj["op"],
1477
- obj["args"],
1478
- obj["kwargs"],
1479
- )
1480
-
1481
- response = None
1482
- if f_op == "filter":
1483
- f_args = [self._deser_filter(x) for x in f_args]
1484
- f_kwargs = {k: self._deser_filter(v) for k, v in f_kwargs}
1485
- response = self.filter(*f_args, **f_kwargs)
1486
- if type(response) is not UDFFrame:
1487
- raise Exception(
1488
- f"filter must return a UDFFrame, got {type(response)}"
1489
- )
1490
- if response.frame_type().pix_fmt() != "rgb24":
1491
- raise Exception(
1492
- f"filter must return a frame with pix_fmt 'rgb24', got {response.frame_type().pix_fmt()}"
1493
- )
1494
-
1495
- response = response._response_ser()
1496
- elif f_op == "filter_type":
1497
- f_args = [self._deser_filter_type(x) for x in f_args]
1498
- f_kwargs = {k: self._deser_filter_type(v) for k, v in f_kwargs}
1499
- response = self.filter_type(*f_args, **f_kwargs)
1500
- if type(response) is not UDFFrameType:
1501
- raise Exception(
1502
- f"filter_type must return a UDFFrameType, got {type(response)}"
1503
- )
1504
- if response.pix_fmt() != "rgb24":
1505
- raise Exception(
1506
- f"filter_type must return a frame with pix_fmt 'rgb24', got {response.pix_fmt()}"
1507
- )
1508
- response = response._response_ser()
1509
- else:
1510
- raise Exception(f"Unknown operation: {f_op}")
1511
-
1512
- response = msgpack.packb(response, use_bin_type=True)
1513
- response_len = len(response).to_bytes(4, byteorder="big")
1514
- connection.sendall(response_len)
1515
- connection.sendall(response)
1516
- finally:
1517
- connection.close()
1518
-
1519
- def _deser_filter_type(self, obj):
1520
- assert type(obj) is dict
1521
- keys = list(obj.keys())
1522
- assert len(keys) == 1
1523
- type_key = keys[0]
1524
- assert type_key in ["FrameType", "String", "Int", "Bool"]
1525
-
1526
- if type_key == "FrameType":
1527
- frame = obj[type_key]
1528
- assert type(frame) is dict
1529
- assert "width" in frame
1530
- assert "height" in frame
1531
- assert "format" in frame
1532
- assert type(frame["width"]) is int
1533
- assert type(frame["height"]) is int
1534
- assert frame["format"] == 2 # AV_PIX_FMT_RGB24
1535
- return UDFFrameType(frame["width"], frame["height"], "rgb24")
1536
- elif type_key == "String":
1537
- assert type(obj[type_key]) is str
1538
- return obj[type_key]
1539
- elif type_key == "Int":
1540
- assert type(obj[type_key]) is int
1541
- return obj[type_key]
1542
- elif type_key == "Bool":
1543
- assert type(obj[type_key]) is bool
1544
- return obj[type_key]
1545
- else:
1546
- assert False, f"Unknown type: {type_key}"
1547
-
1548
- def _deser_filter(self, obj):
1549
- assert type(obj) is dict
1550
- keys = list(obj.keys())
1551
- assert len(keys) == 1
1552
- type_key = keys[0]
1553
- assert type_key in ["Frame", "String", "Int", "Bool"]
1554
-
1555
- if type_key == "Frame":
1556
- frame = obj[type_key]
1557
- assert type(frame) is dict
1558
- assert "data" in frame
1559
- assert "width" in frame
1560
- assert "height" in frame
1561
- assert "format" in frame
1562
- assert type(frame["width"]) is int
1563
- assert type(frame["height"]) is int
1564
- assert frame["format"] == "rgb24"
1565
- assert type(frame["data"]) is bytes
1566
-
1567
- data = np.frombuffer(frame["data"], dtype=np.uint8)
1568
- data = data.reshape(frame["height"], frame["width"], 3)
1569
- return UDFFrame(
1570
- data, UDFFrameType(frame["width"], frame["height"], "rgb24")
1571
- )
1572
- elif type_key == "String":
1573
- assert type(obj[type_key]) is str
1574
- return obj[type_key]
1575
- elif type_key == "Int":
1576
- assert type(obj[type_key]) is int
1577
- return obj[type_key]
1578
- elif type_key == "Bool":
1579
- assert type(obj[type_key]) is bool
1580
- return obj[type_key]
1581
- else:
1582
- assert False, f"Unknown type: {type_key}"
1583
-
1584
- def _host(self, socket_path: str):
1585
- if os.path.exists(socket_path):
1586
- os.remove(socket_path)
1587
-
1588
- # start listening on the socket
1589
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1590
- sock.bind(socket_path)
1591
- sock.listen(1)
1592
-
1593
- while True:
1594
- # accept incoming connection
1595
- connection, client_address = sock.accept()
1596
- thread = threading.Thread(
1597
- target=self._handle_connection, args=(connection,)
1598
- )
1599
- thread.start()
1600
-
1601
- def __del__(self):
1602
- if self._socket_path is not None:
1603
- self._p.terminate()
1604
- if os.path.exists(self._socket_path):
1605
- # it's possible the process hasn't even created the socket yet
1606
- os.remove(self._socket_path)
1607
-
1608
-
1609
- class UDFFrameType:
1610
- """
1611
- Frame type for use in UDFs.
1612
- """
1613
-
1614
- def __init__(self, width: int, height: int, pix_fmt: str):
1615
- assert type(width) is int
1616
- assert type(height) is int
1617
- assert type(pix_fmt) is str
1618
-
1619
- self._width = width
1620
- self._height = height
1621
- self._pix_fmt = pix_fmt
1622
-
1623
- def width(self):
1624
- return self._width
1625
-
1626
- def height(self):
1627
- return self._height
1628
-
1629
- def pix_fmt(self):
1630
- return self._pix_fmt
1631
-
1632
- def _response_ser(self):
1633
- return {
1634
- "frame_type": {
1635
- "width": self._width,
1636
- "height": self._height,
1637
- "format": 2, # AV_PIX_FMT_RGB24
1638
- }
1639
- }
1640
-
1641
- def __repr__(self):
1642
- return f"FrameType<{self._width}x{self._height}, {self._pix_fmt}>"
1643
-
1644
-
1645
- class UDFFrame:
1646
- """A symbolic reference to a frame for use in UDFs."""
1647
-
1648
- def __init__(self, data: np.ndarray, f_type: UDFFrameType):
1649
- assert type(data) is np.ndarray
1650
- assert type(f_type) is UDFFrameType
1651
-
1652
- # We only support RGB24 for now
1653
- assert data.dtype == np.uint8
1654
- assert data.shape[2] == 3
1655
-
1656
- # check type matches
1657
- assert data.shape[0] == f_type.height()
1658
- assert data.shape[1] == f_type.width()
1659
- assert f_type.pix_fmt() == "rgb24"
1660
-
1661
- self._data = data
1662
- self._f_type = f_type
1663
-
1664
- def data(self):
1665
- return self._data
1666
-
1667
- def frame_type(self):
1668
- return self._f_type
1669
-
1670
- def _response_ser(self):
1671
- return {
1672
- "frame": {
1673
- "data": self._data.tobytes(),
1674
- "width": self._f_type.width(),
1675
- "height": self._f_type.height(),
1676
- "format": "rgb24",
1677
- }
1678
- }
1679
-
1680
- def __repr__(self):
1681
- return f"Frame<{self._f_type.width()}x{self._f_type.height()}, {self._f_type.pix_fmt()}>"
1682
-
1683
-
1684
- def _run_udf_host(udf: UDF, socket_path: str):
1685
- udf._host(socket_path)