8000 Merge pull request #2 from video-db/ar/add-player · video-db/videodb-python@0fb1f53 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0fb1f53

Browse files
authored
Merge pull request #2 from video-db/ar/add-player
feat: add player
2 parents 23cffa4 + faaac63 commit 0fb1f53

File tree

9 files changed

+115
-45
lines changed

9 files changed

+115
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ dist/*
1515
venv/
1616
.vscode/*
1717
example.ipynb
18+
example.py

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ video = conn.upload(url="https://www.youtube.com/")
6767
video = conn.upload(file_path="path/to/video.mp4")
6868

6969
# get the stream url for the video
70-
stream_url = video.get_stream()
70+
stream_url = video.generate_stream()
7171

7272
```
7373

@@ -113,7 +113,7 @@ collection = conn.get_collection()
113113
# get the video from the collection
114114
video = collection.get_video("video_id")
115115

116-
# index the video for symantic search
116+
# index the video for semantic search
117117
video.index_spoken_words()
118118

119119
# search relevant moment in video and stream resultant video clip instantly.
@@ -139,7 +139,7 @@ stream_url = result.compile()
139139
# get shots of the result returns a list of Shot objects
140140
shots = result.get_shots()
141141
# get stream url of the shot
142-
short_stream_url = shots[0].get_stream()
142+
short_stream_url = shots[0].generate_stream()
143143

144144
```
145145

@@ -155,10 +155,10 @@ video = collection.get_video("video_id")
155155
# get the stream url of the dynamically curated video based on the given timeline sequence
156156
# optional parameters:
157157
# - timeline: Optional[list[tuple[int, int]] to specify the start and end time of the video
158-
stream_url = video.get_stream(timeline=[(0, 10), (30, 40)])
158+
stream_url = video.generate_stream(timeline=[(0, 10), (30, 40)])
159159

160-
# get thumbnail of the video
161-
thumbnail = video.get_thumbnail()
160+
# get thumbnail url of the video
161+
thumbnail_url = video.generate_thumbnail()
162162

163163
# get transcript of the video
164164
# optional parameters:

videodb/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55

66
from typing import Optional
7+
from videodb._utils._video import play_stream
78
from videodb._constants import VIDEO_DB_API
89
from videodb.client import Connection
910
from videodb.exceptions import (
@@ -23,6 +24,7 @@
2324
"AuthenticationError",
2425
"InvalidRequestError",
2526
"SearchError",
27+
"play_stream",
2628
]
2729

2830

videodb/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
VIDEO_DB_API: str = "https://api.videodb.io"
5+
PLAYER_URL: str = "https://console.videodb.io/player"
56

67

78
class SearchType:

videodb/_utils/_http_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def _handle_request_error(self, e: requests.exceptions.RequestException) -> None
120120
f"Invalid request: {str(e)}", e.response
121121
) from None
122122

123-
@backoff.on_exception(backoff.expo, Exception, max_time=500)
123+
@backoff.on_exception(backoff.expo, Exception, max_time=500, logger=None)
124124
def _get_output(self, url: str):
125125
"""Get the output from an async request"""
126126
response_json = self.session.get(url).json()

videodb/_utils/_video.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import webbrowser as web
2+
3+
from videodb._constants import PLAYER_URL
4+
5+
6+
def play_stream(url: str):
7+
"""Play a stream url in the browser/ notebook
8+
9+
:param str url: The url of the stream
10+
:return: The player url if the stream is opened in the browser or the iframe if the stream is opened in the notebook
11+
"""
12+
player = f"{PLAYER_URL}?url={url}"
13+
opend = web.open(player)
14+
if not opend:
15+
try:
16+
from IPython.display import IFrame
17+
18+
player_width = 800
19+
player_height = 400
20+
return IFrame(player, player_width, player_height)
21+
except ImportError:
22+
return player
23+
return player

videodb/search.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC, abstractmethod
2+
from videodb._utils._video import play_stream
23
from videodb._constants import (
34
SearchType,
45
ApiPath,
@@ -15,8 +16,8 @@ class SearchResult:
1516
def __init__(self, _connection, **kwargs):
1617
self._connection = _connection
1718
self.shots = []
18-
self.text_summary = None
19-
self.stream = None
19+
self.stream_url = None
20+
self.player_url = None
2021
self.collection_id = "default"
2122
self._results = kwargs.get("results", [])
2223
self._format_results()
@@ -38,18 +39,27 @@ def _format_results(self):
3839
)
3940
)
4041

42+
def __repr__(self) -> str:
43+
return (
44+
f"SearchResult("
45+
f"collection_id={self.collection_id}, "
46+
f"stream_url={self.stream_url}, "
47+
f"player_url={self.player_url}, "
48+
f"shots={self.shots})"
49+
)
50+
4151
def get_shots(self) -> List[Shot]:
4252
return self.shots
4353

4454
def compile(self) -> str:
45-
"""Compile the search result shots into a stream link
55+
"""Compile the search result shots into a stream url
4656
4757
:raises SearchError: If no shots are found in the search results
48-
:return: The stream link
58+
:return: The stream url
4959
:rtype: str
5060
"""
51-
if self.stream:
52-
return self.stream
61+
if self.stream_url:
62+
return self.stream_url
5363
elif self.shots:
5464
compile_data = self._connection.post(
5565
path=f"{ApiPath.compile}",
@@ -62,12 +72,22 @@ def compile(self) -> str:
6272
for shot in self.shots
6373
],
6474
)
65-
self.stream = compile_data.get("stream_link")
66-
return self.stream
75+
self.stream_url = compile_data.get("stream_url")
76+
self.player_url = compile_data.get("player_url")
77+
return self.stream_url
6778

6879
else:
6980
raise SearchError("No shots found in search results to compile")
7081

82+
def play(self) -> str:
83+
"""Generate a stream url for the shot and open it in the default browser
84+
85+
:return: The stream url
86+
:rtype: str
87+
"""
88+
self.compile()
89+
return play_stream(self.stream_url)
90+
7191

7292
class Search(ABC):
7393
"""Search interface inside video or collection"""

videodb/shot.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
from typing import Optional
5+
from videodb._utils._video import play_stream
56
from videodb._constants import (
67
ApiPath,
78
)
@@ -29,7 +30,8 @@ def __init__(
2930
self.end = end
3031
self.text = text
3132
self.search_score = search_score
32-
self.stream = None
33+
self.stream_url = None
34+
self.player_url = None
3335

3436
def __repr__(self) -> str:
3537
return (
@@ -40,22 +42,23 @@ def __repr__(self) -> str:
4042
f"end={self.end}, "
4143
f"text={self.text}, "
4244
f"search_score={self.search_score}, "
43-
f"stream={self.stream})"
45+
f"stream_url={self.stream_url}, "
46+
f"player_url={self.player_url})"
4447
)
4548

4649
def __getitem__(self, key):
4750
"""Get an item from the shot object"""
4851
return self.__dict__[key]
4952

50-
def get_stream(self) -> str:
51-
"""Get the shot into a stream link
53+
def generate_stream(self) -> str:
54+
"""Generate a stream url for the shot
5255
53-
:return: The stream link
56+
:return: The stream url
5457
:rtype: str
5558
"""
5659

57-
if self.stream:
58-
return self.stream
60+
if self.stream_url:
61+
return self.stream_url
5962
else:
6063
stream_data = self._connection.post(
6164
path=f"{ApiPath.video}/{self.video_id}/{ApiPath.stream}",
@@ -64,5 +67,15 @@ def get_stream(self) -> str:
6467
"length": self.video_length,
6568
},
6669
)
67-
self.stream = stream_data.get("stream_link")
68-
return self.stream
70+
self.stream_url = stream_data.get("stream_url")
71+
self.player_url = stream_data.get("player_url")
72+
return self.stream_url
73+
74+
def play(self) -> str:
75+
"""Generate a stream url for the shot and open it in the default browser/ notebook
76+
77+
:return: The stream url
78+
:rtype: str
79+
"""
80+
self.generate_stream()
81+
return play_stream(self.stream_url)

videodb/video.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
from videodb._utils._video import play_stream
13
from videodb._constants import (
24
ApiPath,
35
SearchType,
@@ -6,18 +8,18 @@
68
)
79
from videodb.search import SearchFactory, SearchResult
810
from videodb.shot import Shot
9-
from typing import Optional
1011

1112

1213
class Video:
1314
def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None:
1415
self._connection = _connection
1516
self.id = id
1617
self.collection_id = collection_id
17-
self.stream_link = kwargs.get("stream_link", None)
18+
self.stream_url = kwargs.get("stream_url", None)
19+
self.player_url = kwargs.get("player_url", None)
1820
self.name = kwargs.get("name", None)
1921
self.description = kwargs.get("description", None)
20-
self.thumbnail = kwargs.get("thumbnail", None)
22+
self.thumbnail_url = kwargs.get("thumbnail_url", None)
2123
self.length = float(kwargs.get("length", 0.0))
2224
self.transcript = kwargs.get("transcript", None)
2325
self.transcript_text = kwargs.get("transcript_text", None)
@@ -27,10 +29,11 @@ def __repr__(self) -> str:
2729
f"Video("
2830
f"id={self.id}, "
2931
f"collection_id={self.collection_id}, "
30-
f"stream_link={self.stream_link}, "
32+
f"stream_url={self.stream_url}, "
33+
f"player_url={self.player_url}, "
3134
f"name={self.name}, "
3235
f"description={self.description}, "
33-
f"thumbnail={self.thumbnail}, "
36+
f"thumbnail_url={self.thumbnail_url}, "
3437
f"length={self.length})"
3538
)
3639

@@ -63,16 +66,16 @@ def delete(self) -> None:
6366
"""
6467
self._connection.delete(path=f"{ApiPath.video}/{self.id}")
6568

66-
def get_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str:
67-
"""Get the stream link of the video
69+
def generate_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str:
70+
"""Generate the stream url of the video
6871
6972
:param list timeline: The timeline of the video to be streamed. Defaults to None.
7073
:raises InvalidRequestError: If the get_stream fails
71-
:return: The stream link of the video
74+
:return: The stream url of the video
7275
:rtype: str
7376
"""
74-
if not timeline and self.stream_link:
75-
return self.stream_link
77+
if not timeline and self.stream_url:
78+
return self.stream_url
7679

7780
stream_data = self._connection.post(
7881
path=f"{ApiPath.video}/{self.id}/{ApiPath.stream}",
@@ -81,16 +84,16 @@ def get_stream(self, timeline: Optional[list[tuple[int, int]]] = None) -> str:
8184
"length": self.length,
8285
},
8386
)
84-
return stream_data.get("stream_link")
87+
return stream_data.get("stream_url", None)
8588

86-
def get_thumbnail(self):
87-
if 10000 self.thumbnail:
88-
return self.thumbnail
89+
def generate_thumbnail(self):
90+
if self.thumbnail_url:
91+
return self.thumbnail_url
8992
thumbnail_data = self._connection.get(
9093
path=f"{ApiPath.video}/{self.id}/{ApiPath.thumbnail}"
9194
)
92-
self.thumbnail = thumbnail_data.get("thumbnail")
93-
return self.thumbnail
95+
self.thumbnail_url = thumbnail_data.get("thumbnail_url")
96+
return self.thumbnail_url
9497

9598
def _fetch_transcript(self, force: bool = False) -> None:
9699
if self.transcript and not force:
@@ -111,7 +114,7 @@ def get_transcript_text(self, force: bool = False) -> str:
111114
return self.transcript_text
112115

113116
def index_spoken_words(self) -> None:
114-
"""Symantic indexing of spoken words in the video
117+
"""Semantic indexing of spoken words in the video
115118
116119
:raises InvalidRequestError: If the video is already indexed
117120
:return: None if the indexing is successful
@@ -132,15 +135,15 @@ def add_subtitle(self) -> str:
132135
"type": Workflows.add_subtitles,
133136
},
134137
)
135-
return subtitle_data.get("stream_link")
138+
return subtitle_data.get("stream_url", None)
136139

137140
def insert_video(self, video, timestamp: float) -> str:
138141
"""Insert a video into another video
139142
140143
:param Video video: The video to be inserted
141144
:param float timestamp: The timestamp where the video should be inserted
142145
:raises InvalidRequestError: If the insert fails
143-
:return: The stream link of the inserted video
146+
:return: The stream url of the inserted video
144147
:rtype: str
145148
"""
146149
if timestamp > float(self.length):
@@ -171,5 +174,12 @@ def insert_video(self, video, timestamp: float) -> str:
171174
for shot in all_shots
172175
],
173176
)
174-
stream_link = compile_data.get("stream_link")
175-
return stream_link
177+
return compile_data.get("stream_url", None)
178+
179+
def play(self) -> str:
180+
"""Open the player url in the browser/iframe and return the stream url
181+
182+
:return: The stream url
183+
:rtype: str
184+
"""
185+
return play_stream(self.stream_url)

0 commit comments

Comments
 (0)
0