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 +56 -836
- vidformer/cv2/__init__.py +99 -170
- vidformer/supervision/__init__.py +91 -1
- {vidformer-0.11.0.dist-info → vidformer-1.0.0.dist-info}/METADATA +2 -3
- vidformer-1.0.0.dist-info/RECORD +6 -0
- {vidformer-0.11.0.dist-info → vidformer-1.0.0.dist-info}/WHEEL +1 -1
- vidformer-0.11.0.dist-info/RECORD +0 -6
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
|
+
__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
|
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"
|
413
|
+
return f"Source({self._name})"
|
424
414
|
|
425
415
|
|
426
|
-
class
|
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,
|
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,
|
435
|
+
return _play(self._id, url, hls_js_url, method=method, status_url=status_url)
|
446
436
|
|
447
437
|
|
448
|
-
class
|
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) ->
|
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
|
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
|
-
) ->
|
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) ->
|
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) ->
|
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
|
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
|
-
) ->
|
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
|
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
|
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,
|
1363
|
-
self.
|
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)
|