Skip to content

Commit

Permalink
wip: Support streaming from video services
Browse files Browse the repository at this point in the history
Relates to #2186
  • Loading branch information
postlund committed Sep 13, 2023
1 parent 6704359 commit e185ecf
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 183 deletions.
18 changes: 17 additions & 1 deletion docs/api/pyatv.exceptions.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ <h4><code><a title="pyatv.exceptions.InvalidCredentialsError" href="#pyatv.excep
<h4><code><a title="pyatv.exceptions.InvalidDmapDataError" href="#pyatv.exceptions.InvalidDmapDataError">InvalidDmapDataError</a></code></h4>
</li>
<li>
<h4><code><a title="pyatv.exceptions.InvalidFormatError" href="#pyatv.exceptions.InvalidFormatError">InvalidFormatError</a></code></h4>
</li>
<li>
<h4><code><a title="pyatv.exceptions.InvalidResponseError" href="#pyatv.exceptions.InvalidResponseError">InvalidResponseError</a></code></h4>
</li>
<li>
Expand Down Expand Up @@ -108,7 +111,7 @@ <h1 class="title">Module <code>pyatv.exceptions</code></h1>
</header>
<section id="section-intro">
<p>Local exceptions used by library.</p>
<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/exceptions.py#L1-L131" class="git-link">Browse git</a></div>
<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/exceptions.py#L1-L135" class="git-link">Browse git</a></div>
</section>
<section>
</section>
Expand Down Expand Up @@ -275,6 +278,19 @@ <h3>Ancestors</h3>
<li>builtins.BaseException</li>
</ul>
</dd>
<dt id="pyatv.exceptions.InvalidFormatError"><code class="flex name class">
<span>class <span class="ident">InvalidFormatError</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<section class="desc"><p>Raised when an unsupported (file) format is encountered.</p></section>
<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/exceptions.py#L134-L135" class="git-link">Browse git</a></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>builtins.Exception</li>
<li>builtins.BaseException</li>
</ul>
</dd>
<dt id="pyatv.exceptions.InvalidResponseError"><code class="flex name class">
<span>class <span class="ident">InvalidResponseError</span></span>
<span>(</span><span>*args, **kwargs)</span>
Expand Down
378 changes: 198 additions & 180 deletions docs/api/pyatv.interface.html

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyatv/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,7 @@ class OperationTimeoutError(Exception):

class SettingsError(Exception):
"""Raised when an error related to settings happens."""


class InvalidFormatError(Exception):
"""Raised when an unsupported (file) format is encountered."""
19 changes: 19 additions & 0 deletions pyatv/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from pyatv.support.device_info import lookup_version
from pyatv.support.http import ClientSessionManager
from pyatv.support.state_producer import StateProducer
from pyatv.support.yt_dlp import extract_video_url

__pdoc__ = {
"feature": False,
Expand Down Expand Up @@ -874,6 +875,24 @@ async def stream_file(
"""
raise exceptions.NotSupportedError()

async def play_service(self, video_url: str) -> None:
"""Play video from a video service, e.g. YouTube.
This method will try to extract the underlying video URL from various video
hosting services, e.g. YouTube, and play the video using play_url.
Note 1: For this method to work, yt-dlp must be installed. A NotSupportedError
is thrown otherwise.
Note 2: By default, pyatv will try to play the video with highest bitrate. It's
not possible to possible to change this at the moment, but will be in the
future.
INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!
"""
url = await extract_video_url(video_url)
await self.play_url(url)


class DeviceListener(ABC):
"""Listener interface for generic device updates."""
Expand Down
3 changes: 2 additions & 1 deletion pyatv/protocols/airplay/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
_LOGGER = logging.getLogger(__name__)

PLAY_RETRIES = 3
WAIT_RETRIES = 5
WAIT_RETRIES = 10

HEADERS = {
"User-Agent": "AirPlay/550.10",
"Content-Type": "application/x-apple-binary-plist",
Expand Down
63 changes: 63 additions & 0 deletions pyatv/support/yt_dlp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Helper methods for working with yt-dlp.
Currently ytp-dl is used to extract video URLs from various video sites, e.g. YouTube
so they can be streamed via AirPlay.
"""
import asyncio

from pyatv import exceptions


def _extract_video_url(video_link: str) -> str:
# TODO: For now, dynamic support for this feature. User must manually install
# yt-dlp, it will not be pulled in by pyatv.
try:
import yt_dlp # pylint: disable=import-outside-toplevel
except ModuleNotFoundError as ex:
raise exceptions.NotSupportedError("package yt-dlp not installed") from ex

with yt_dlp.YoutubeDL({"quiet": True, "no_warnings": True}) as ydl:
info = ydl.sanitize_info(ydl.extract_info(video_link, download=False))

if "formats" not in info:
raise exceptions.NotSupportedError(
"formats are missing, maybe authentication is needed (not supported)?"
)

best = None
best_bitrate = 0

# Try to find supported video stream with highest bitrate. No way to customize
# this in any way for now.
for video_format in [
x for x in info["formats"] if x.get("protocol") == "m3u8_native"
]:
if video_format["video_ext"] == "none":
continue
if video_format["has_drm"]:
continue

if video_format["vbr"] > best_bitrate:
best = video_format
best_bitrate = video_format["vbr"]

if not best or "manifest_url" not in best:
raise exceptions.NotSupportedError("manifest url could not be extracted")

return best["manifest_url"]


async def extract_video_url(video_link: str) -> str:
"""Extract video URL from external video service link.
This method takes a video link from a video service, e.g. YouTube, and extracts the
underlying video URL that (hopefully) can be played via AirPlay. Currently yt-dlp
is used to the extract the URL, thus all services supported by yt-dlp should be
supported. No customization (e.g. resolution) nor authorization is supported at the
moment, putting some restrictions on use case.
"""
loop = asyncio.get_event_loop()
try:
return await loop.run_in_executor(None, _extract_video_url, video_link)
except Exception as ex:
raise exceptions.InvalidFormatError(f"video {video_link} not supported") from ex
2 changes: 1 addition & 1 deletion tests/core/test_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ async def test_base_methods_guarded_after_close(facade_dummy, register_interface
(RemoteControl, "remote_control", {}),
(Metadata, "metadata", {}),
(PushUpdater, "push_updater", {}),
(Stream, "stream", {}),
(Stream, "stream", {"play_service"}),
(Power, "power", {}),
# in_states is not abstract but uses get_features, will which will raise
(Features, "features", {"in_state"}),
Expand Down

0 comments on commit e185ecf

Please sign in to comment.