Skip to content

Commit

Permalink
finish broadcast, refactoring common code
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkahan committed Oct 4, 2024
1 parent 91e5dc4 commit 94e3dd6
Show file tree
Hide file tree
Showing 15 changed files with 630 additions and 93 deletions.
5 changes: 0 additions & 5 deletions number_insight_v2/src/vonage_number_insight_v2/errors.py

This file was deleted.

4 changes: 4 additions & 0 deletions video/src/vonage_video/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ class InvalidHlsOptionsError(VideoError):

class InvalidOutputOptionsError(VideoError):
"""The output options were invalid."""


class InvalidBroadcastStateError(VideoError):
"""The broadcast state was invalid for the specified operation."""
72 changes: 53 additions & 19 deletions video/src/vonage_video/models/broadcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class BroadcastRtmp(BaseModel):
Args:
id (str, Optional): A unique ID for the stream.
server_url (str): The RTMP server URL.
stream_name (str): The such as the YouTube Live stream name or the Facebook stream key.
stream_name (str): The stream name, such as the YouTube Live stream name or the
Facebook stream key.
"""

id: Optional[str] = None
Expand All @@ -57,10 +58,13 @@ class RtmpStream(BroadcastRtmp):
Args:
id (str, Optional): A unique ID for the stream.
server_url (str): The RTMP server URL.
stream_name (str): The such as the YouTube Live stream name or the Facebook stream key.
stream_name (str): The stream name, such as the YouTube Live stream name or the
Facebook stream key.
status (str, Optional): The status of the RTMP stream.
"""

server_url: Optional[str] = Field(None, validation_alias='serverUrl')
stream_name: Optional[str] = Field(None, validation_alias='streamName')
status: Optional[str] = None


Expand All @@ -69,13 +73,37 @@ class BroadcastUrls(BaseModel):
Args:
hls (str, Optional): URL for the HLS broadcast.
hls_status (str, Optional): The status of the HLS broadcast.
rtmp (List[str], Optional): An array of objects that include information on each of the RTMP streams.
"""

hls: Optional[str] = None
hls_status: Optional[str] = Field(None, validation_alias='hlsStatus')
rtmp: Optional[List[RtmpStream]] = None


class HlsSettings(BaseModel):
"""Model for HLS settings for a broadcast.
Args:
dvr (bool, Optional): Whether the broadcast supports DVR.
low_latency (bool, Optional): Whether the broadcast is low latency.
"""

dvr: Optional[bool] = None
low_latency: Optional[bool] = Field(None, validation_alias='lowLatency')


class BroadcastSettings(BaseModel):
"""Model for settings for a broadcast.
Args:
hls (HlsSettings, Optional): HLS settings for the broadcast.
"""

hls: Optional[HlsSettings] = None


class Broadcast(BaseModel):
"""Model for a broadcast.
Expand Down Expand Up @@ -114,7 +142,7 @@ class Broadcast(BaseModel):
max_duration: Optional[int] = Field(None, alias='maxDuration')
max_bitrate: Optional[int] = Field(None, alias='maxBitrate')
broadcast_urls: Optional[BroadcastUrls] = Field(None, alias='broadcastUrls')
settings: Optional[BroadcastHls] = None
settings: Optional[BroadcastSettings] = None
resolution: Optional[VideoResolution] = None
has_audio: Optional[bool] = Field(None, alias='hasAudio')
has_video: Optional[bool] = Field(None, alias='hasVideo')
Expand All @@ -123,7 +151,7 @@ class Broadcast(BaseModel):
streams: Optional[List[VideoStream]] = None


class Outputs(BaseModel):
class BroadcastOutputSettings(BaseModel):
"""Model for output options for a broadcast. You must specify at least one output option.
Args:
Expand All @@ -148,28 +176,34 @@ class CreateBroadcastRequest(BaseModel):
Args:
session_id (str): The session ID of a Vonage Video session.
has_audio (bool, Optional): Whether the archive or broadcast should include audio.
has_video (bool, Optional): Whether the archive or broadcast should include video.
layout (Layout, Optional): Layout options for the archive or broadcast.
name (str, Optional): The name of the archive or broadcast.
output_mode (OutputMode, Optional): Whether all streams in the archive or broadcast are recorded to a
single file ("composed", the default) or to individual files ("individual").
resolution (VideoResolution, Optional): The resolution of the archive or broadcast.
stream_mode (StreamMode, Optional): Whether streams included in the archive or broadcast are selected
layout (Layout, Optional): Layout options for the broadcast.
max_duration (int, Optional): The maximum duration of the broadcast in seconds.
outputs (Outputs): Output options for the broadcast. This object defines the types of
broadcast streams you want to start (both HLS and RTMP). You can include HLS, RTMP,
or both as broadcast streams. If you include RTMP streaming, you can specify up
to five target RTMP streams (or just one). Vonage streams the session to each RTMP
URL you specify. Note that Vonage Video live streaming supports RTMP and RTMPS.
resolution (VideoResolution, Optional): The resolution of the broadcast.
stream_mode (StreamMode, Optional): Whether streams included in the broadcast are selected
automatically ("auto", the default) or manually ("manual").
multi_broadcast_tag (str, Optional): Set this to support recording multiple broadcasts for the same session simultaneously.
Set this to a unique string for each simultaneous broadcast of an ongoing session.
You must also set this option when manually starting a broadcast in a session that is automatically broadcasted.
If you do not specify a unique multiBroadcastTag, you can only record one broadcast at a time for a given session.
multi_broadcast_tag (str, Optional): Set this to support recording multiple broadcasts
for the same session simultaneously. Set this to a unique string for each simultaneous
broadcast of an ongoing session. If you do not specify a unique multiBroadcastTag,
you can only record one broadcast at a time for a given session.
max_bitrate (int, Optional): The maximum bitrate of the broadcast, in bits per second.
"""

session_id: str = Field(..., serialization_alias='sessionId')
layout: Optional[ComposedLayout] = None
max_duration: Optional[int] = Field(None, serialization_alias='maxDuration')
outputs: Outputs
max_duration: Optional[int] = Field(
None, ge=60, le=36000, serialization_alias='maxDuration'
)
outputs: BroadcastOutputSettings
resolution: Optional[VideoResolution] = None
stream_mode: Optional[StreamMode] = Field(None, serialization_alias='streamMode')
multi_broadcast_tag: Optional[str] = Field(
None, serialization_alias='multiBroadcastTag'
)
max_bitrate: Optional[int] = Field(None, serialization_alias='maxBitrate')
max_bitrate: Optional[int] = Field(
None, ge=100_000, le=6_000_000, serialization_alias='maxBitrate'
)
156 changes: 96 additions & 60 deletions video/src/vonage_video/video.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Type, Union

from pydantic import validate_call
from vonage_http_client.errors import HttpRequestError
from vonage_http_client.http_client import HttpClient
from vonage_video.errors import InvalidArchiveStateError
from vonage_video.errors import (
InvalidArchiveStateError,
InvalidBroadcastStateError,
VideoError,
)
from vonage_video.models.archive import (
Archive,
ComposedLayout,
Expand All @@ -17,7 +21,7 @@
ListBroadcastsFilter,
)
from vonage_video.models.captions import CaptionsData, CaptionsOptions
from vonage_video.models.common import AddStreamRequest, VideoStream
from vonage_video.models.common import AddStreamRequest
from vonage_video.models.experience_composer import (
ExperienceComposer,
ExperienceComposerOptions,
Expand Down Expand Up @@ -317,20 +321,7 @@ def list_experience_composers(
filter.model_dump(exclude_none=True, by_alias=True),
)

index = filter.offset + 1 or 1
page_size = filter.page_size
experience_composers = []

try:
for ec in response['items']:
experience_composers.append(ExperienceComposer(**ec))
except KeyError:
return [], 0, None

count = response['count']
if count > page_size * (index):
return experience_composers, count, index
return experience_composers, count, None
return self._list_video_objects(filter, response, ExperienceComposer)

@validate_call
def get_experience_composer(self, experience_composer_id: str) -> ExperienceComposer:
Expand Down Expand Up @@ -382,20 +373,7 @@ def list_archives(
filter.model_dump(exclude_none=True, by_alias=True),
)

index = filter.offset + 1 or 1
page_size = filter.page_size
archives = []

try:
for archive in response['items']:
archives.append(Archive(**archive))
except KeyError:
return [], 0, None

count = response['count']
if count > page_size * (index):
return archives, count, index
return archives, count, None
return self._list_video_objects(filter, response, Archive)

@validate_call
def start_archive(self, options: CreateArchiveRequest) -> Archive:
Expand Down Expand Up @@ -448,11 +426,10 @@ def delete_archive(self, archive_id: str) -> None:
f'/v2/project/{self._http_client.auth.application_id}/archive/{archive_id}',
)
except HttpRequestError as e:
if e.response.status_code == 409:
raise InvalidArchiveStateError(
'You can only delete an archive that has one of the following statuses: `available` OR `uploaded` OR `deleted`.'
)
raise e
conflict_error_message = 'You can only delete an archive that has one of the following statuses: `available` OR `uploaded` OR `deleted`.'
self._check_conflict_error(
e, InvalidArchiveStateError, conflict_error_message
)

@validate_call
def add_stream_to_archive(self, archive_id: str, params: AddStreamRequest) -> None:
Expand Down Expand Up @@ -502,11 +479,12 @@ def stop_archive(self, archive_id: str) -> Archive:
f'/v2/project/{self._http_client.auth.application_id}/archive/{archive_id}/stop',
)
except HttpRequestError as e:
if e.response.status_code == 409:
raise InvalidArchiveStateError(
'You can only stop an archive that is being recorded.'
)
raise e
conflict_error_message = (
'You can only stop an archive that is being recorded.'
)
self._check_conflict_error(
e, InvalidArchiveStateError, conflict_error_message
)
return Archive(**response)

@validate_call
Expand Down Expand Up @@ -550,20 +528,7 @@ def list_broadcasts(
filter.model_dump(exclude_none=True, by_alias=True),
)

index = filter.offset + 1 or 1
page_size = filter.page_size
broadcasts = []

try:
for broadcast in response['items']:
broadcasts.append(Broadcast(**broadcast))
except KeyError:
return [], 0, None

count = response['count']
if count > page_size * (index):
return broadcasts, count, index
return broadcasts, count, None
return self._list_video_objects(filter, response, Broadcast)

@validate_call
def start_broadcast(self, options: CreateBroadcastRequest) -> Broadcast:
Expand All @@ -574,12 +539,27 @@ def start_broadcast(self, options: CreateBroadcastRequest) -> Broadcast:
Returns:
Broadcast: The broadcast object.
Raises:
InvalidBroadcastStateError: If the broadcast has already started for the session,
or if you attempt to start a simultaneous broadcast for a session without setting
a unique `multi-broadcast-tag` value.
"""
response = self._http_client.post(
self._http_client.video_host,
f'/v2/project/{self._http_client.auth.application_id}/broadcast',
options.model_dump(exclude_none=True, by_alias=True),
)
try:
response = self._http_client.post(
self._http_client.video_host,
f'/v2/project/{self._http_client.auth.application_id}/broadcast',
options.model_dump(exclude_none=True, by_alias=True),
)
except HttpRequestError as e:
conflict_error_message = (
'Either the broadcast has already started for the session, '
'or you attempted to start a simultaneous broadcast for a session '
'without setting a unique `multi-broadcast-tag` value.'
)
self._check_conflict_error(
e, InvalidBroadcastStateError, conflict_error_message
)

return Broadcast(**response)

Expand Down Expand Up @@ -668,3 +648,59 @@ def remove_stream_from_broadcast(self, broadcast_id: str, stream_id: str) -> Non
f'/v2/project/{self._http_client.auth.application_id}/broadcast/{broadcast_id}/streams',
params={'removeStream': stream_id},
)

@validate_call
def _list_video_objects(
self,
request_filter: Union[
ListArchivesFilter, ListBroadcastsFilter, ListExperienceComposersFilter
],
response: dict,
model: Union[Type[Archive], Type[Broadcast], Type[ExperienceComposer]],
) -> Tuple[List[object], int, Optional[int]]:
"""List objects of a specific model from a response.
Args:
request_filter (Union[ListArchivesFilter, ListBroadcastsFilter, ListExperienceComposersFilter]):
The filter used to make the request.
response (dict): The response from the API.
model (Union[Type[Archive], Type[Broadcast], Type[ExperienceComposer]]): The type of a pydantic
model to populate the response into.
Returns:
Tuple[List[object], int, Optional[int]]: A tuple containing a list of objects,
the total count of objects and the required offset value for the next page, if applicable.
i.e.
objects: List[object], count: int, next_page_offset: Optional[int]
"""
index = request_filter.offset + 1 or 1
page_size = request_filter.page_size
objects = []

try:
for obj in response['items']:
objects.append(model(**obj))
except KeyError:
return [], 0, None

count = response['count']
if count > page_size * index:
return objects, count, index
return objects, count, None

def _check_conflict_error(
self,
http_error: HttpRequestError,
ConflictError: Type[VideoError],
conflict_error_message: str,
) -> None:
"""Checks if the error is a conflict error and raises the specified error.
Args:
http_error (HttpRequestError): The error to check.
ConflictError (Type[VideoError]): The error to raise if there is a conflict.
conflict_error_message (str): The error message if there is a conflict.
"""
if http_error.response.status_code == 409:
raise ConflictError(f'{conflict_error_message} {http_error.response.text}')
raise http_error
33 changes: 33 additions & 0 deletions video/tests/data/broadcast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"id": "f03fad17-4591-4422-8bd3-00a4df1e616a",
"sessionId": "test_session_id",
"applicationId": "test_application_id",
"createdAt": 1728039361014,
"broadcastUrls": {
"rtmp": [
{
"status": "connecting",
"id": "test",
"serverUrl": "rtmp://a.rtmp.youtube.com/live2",
"streamName": "stream-key"
}
],
"hls": "https://broadcast2-euw1-cdn.media.prod.tokbox.com/broadcast-57d6569497-dj9b2.10293/broadcast-57d6569497-dj9b2.10293_f03fad17-4591-4422-8bd3-00a4df1e616a_29f760f8-7ce1-46c9-ade3-f2dedee4ed5f.2_MX4yOWY3NjBmOC03Y2UxLTQ2YzktYWRlMy1mMmRlZGVlNGVkNWZ-fjE3MjgwMzY0MTUzMDd-V2swbzlzeUppaGZIVTFzYUQwamdYM0Ryfn5-.smil/playlist.m3u8?DVR"
},
"updatedAt": 1728039361511,
"status": "started",
"streamMode": "auto",
"hasAudio": true,
"hasVideo": true,
"maxDuration": 3600,
"multiBroadcastTag": "test-broadcast-5",
"maxBitrate": 1000000,
"settings": {
"hls": {
"lowLatency": false,
"dvr": true
}
},
"event": "broadcast",
"resolution": "1280x720"
}
Loading

0 comments on commit 94e3dd6

Please sign in to comment.