diff --git a/videodb/__init__.py b/videodb/__init__.py index a5adc9c..59d2a1b 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -4,7 +4,7 @@ import logging from typing import Optional -from videodb._utils._video import play_stream +from videodb._utils._video import play_stream, build_iframe_embed_code from videodb._constants import ( VIDEO_DB_API, IndexType, @@ -55,6 +55,7 @@ "IndexType", "SearchError", "play_stream", + "build_iframe_embed_code", "MediaType", "SearchType", "SubtitleAlignment", diff --git a/videodb/_utils/_video.py b/videodb/_utils/_video.py index 9cdb012..098b697 100644 --- a/videodb/_utils/_video.py +++ b/videodb/_utils/_video.py @@ -1,7 +1,74 @@ import webbrowser as web +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode + PLAYER_URL: str = "https://console.videodb.io/player" +def player_url_to_embed_url(player_url: str) -> str: + """Convert a /watch player URL to an /embed URL. + + :param str player_url: The player URL (e.g., https://player.videodb.io/watch?v=slug) + :return: The embed URL (e.g., https://player.videodb.io/embed?v=slug) + :rtype: str + :raises ValueError: If the URL format is invalid or missing the 'v' parameter + """ + if not player_url: + raise ValueError("player_url is required to generate embed URL") + + parsed = urlparse(player_url) + query_params = parse_qs(parsed.query) + + if "v" not in query_params: + raise ValueError("player_url must contain a 'v' query parameter") + + embed_params = {"v": query_params["v"][0]} + + embed_url = urlunparse(( + parsed.scheme, + parsed.netloc, + "/embed", + "", + urlencode(embed_params), + "", + )) + + return embed_url + + +def build_iframe_embed_code( + player_url: str, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, +) -> str: + """Build an iframe embed HTML string from a player URL. + + :param str player_url: The player URL to embed + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is empty or height is not positive + """ + if not player_url: + raise ValueError("player_url is required to generate embed code") + + if height <= 0: + raise ValueError("height must be a positive integer") + + embed_url = player_url_to_embed_url(player_url) + fullscreen_attr = " allowfullscreen" if allow_fullscreen else "" + + return ( + f'' + ) + + def play_stream(url: str): """Play a stream url in the browser/ notebook diff --git a/videodb/editor.py b/videodb/editor.py index 5c40282..6c87d53 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -5,6 +5,7 @@ from enum import Enum from videodb._constants import ApiPath +from videodb._utils._video import build_iframe_embed_code from videodb.exceptions import InvalidRequestError @@ -1164,3 +1165,38 @@ def download_stream(self, stream_url: str) -> dict: return self.connection.post( path=f"{ApiPath.editor}/{ApiPath.download}", data={"stream_url": stream_url} ) + + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the timeline. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-generate it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.generate_stream() + + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) diff --git a/videodb/rtstream.py b/videodb/rtstream.py index 540844f..0cf35c6 100644 --- a/videodb/rtstream.py +++ b/videodb/rtstream.py @@ -5,7 +5,7 @@ SceneExtractionType, Segmenter, ) -from videodb._utils._video import play_stream +from videodb._utils._video import play_stream, build_iframe_embed_code class RTStreamSearchResult: @@ -71,6 +71,36 @@ def __repr__(self) -> str: f"duration={self.duration})" ) + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the exported recording. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url: + raise ValueError( + "player_url not available. Export may have failed or returned audio-only content." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) + class RTStreamShot: """RTStreamShot class for rtstream search results @@ -159,6 +189,41 @@ def play(self) -> str: self.generate_stream() return play_stream(self.stream_url) + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the rtstream shot. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-generate it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.generate_stream() + + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) + class RTStreamSceneIndex: """RTStreamSceneIndex class to interact with the rtstream scene index @@ -463,6 +528,40 @@ def generate_stream( self.player_url = stream_data.get("player_url") return self.player_url + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the rtstream. + + Note: Unlike other objects, RTStream does not support auto_generate + because generate_stream() requires start and end parameters. + Call generate_stream(start, end) first to populate player_url. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream(start, end) first to generate a stream." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) + def index_scenes( self, extraction_type=SceneExtractionType.time_based, diff --git a/videodb/search.py b/videodb/search.py index 94730ec..0106985 100644 --- a/videodb/search.py +++ b/videodb/search.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from videodb._utils._video import play_stream +from videodb._utils._video import play_stream, build_iframe_embed_code from videodb._constants import ( IndexType, SearchType, @@ -100,6 +100,41 @@ def play(self) -> str: self.compile() return play_stream(self.stream_url) + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the search result. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-compile it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.compile() + + if not self.player_url: + raise ValueError( + "player_url not available. Call compile() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) + class Search(ABC): """Search interface inside video or collection""" diff --git a/videodb/shot.py b/videodb/shot.py index b261077..643267f 100644 --- a/videodb/shot.py +++ b/videodb/shot.py @@ -1,5 +1,5 @@ from typing import Optional -from videodb._utils._video import play_stream +from videodb._utils._video import play_stream, build_iframe_embed_code from videodb._constants import ( ApiPath, ) @@ -105,3 +105,38 @@ def play(self) -> str: """ self.generate_stream() return play_stream(self.stream_url) + + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the shot. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-generate it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.generate_stream() + + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) diff --git a/videodb/timeline.py b/videodb/timeline.py index f1d1887..9bbeb70 100644 --- a/videodb/timeline.py +++ b/videodb/timeline.py @@ -1,6 +1,7 @@ from typing import Union from videodb._constants import ApiPath +from videodb._utils._video import build_iframe_embed_code from videodb.asset import VideoAsset, AudioAsset, ImageAsset, TextAsset @@ -71,3 +72,38 @@ def generate_stream(self) -> str: self.stream_url = stream_data.get("stream_url") self.player_url = stream_data.get("player_url") return stream_data.get("stream_url", None) + + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the timeline. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-generate it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.generate_stream() + + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) diff --git a/videodb/video.py b/videodb/video.py index 3c7126d..4dcd632 100644 --- a/videodb/video.py +++ b/videodb/video.py @@ -1,5 +1,5 @@ from typing import Optional, Union, List, Dict, Tuple, Any -from videodb._utils._video import play_stream +from videodb._utils._video import play_stream, build_iframe_embed_code from videodb._constants import ( ApiPath, IndexType, @@ -137,7 +137,9 @@ def generate_stream( "length": self.length, }, ) - return stream_data.get("stream_url", None) + self.stream_url = stream_data.get("stream_url") + self.player_url = stream_data.get("player_url") + return self.stream_url def generate_thumbnail(self, time: Optional[float] = None) -> Union[str, Image]: """Generate the thumbnail of the video. @@ -770,6 +772,41 @@ def play(self) -> str: """ return play_stream(self.stream_url) + def get_embed_code( + self, + width: str = "100%", + height: int = 405, + title: str = "VideoDB Player", + allow_fullscreen: bool = True, + auto_generate: bool = True, + ) -> str: + """Generate an HTML iframe embed code for the video. + + :param str width: Width of the iframe (default: "100%") + :param int height: Height of the iframe in pixels (default: 405) + :param str title: Title attribute for the iframe (default: "VideoDB Player") + :param bool allow_fullscreen: Whether to allow fullscreen (default: True) + :param bool auto_generate: If True and player_url is missing, auto-generate it (default: True) + :return: HTML iframe string + :rtype: str + :raises ValueError: If player_url is not available + """ + if not self.player_url and auto_generate: + self.generate_stream() + + if not self.player_url: + raise ValueError( + "player_url not available. Call generate_stream() first or set auto_generate=True." + ) + + return build_iframe_embed_code( + player_url=self.player_url, + width=width, + height=height, + title=title, + allow_fullscreen=allow_fullscreen, + ) + def get_meeting(self): """Get meeting information associated with the video.