Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

companion "precise" skip implementation #2462

Merged
merged 7 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions pyatv/core/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,22 @@ async def wakeup(self) -> None:
return await self.relay("wakeup")()

@shield.guard
async def skip_forward(self) -> None:
async def skip_forward(self, time_interval: float = 0.0) -> None:
"""Skip forward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
If time_interval is not positive or not present, a default or app-chosen
time interval is used, which is typically 10, 15, 30, etc. seconds.
"""
return await self.relay("skip_forward")()
return await self.relay("skip_forward")(time_interval)

@shield.guard
async def skip_backward(self) -> None:
"""Skip backwards a time interval.
async def skip_backward(self, time_interval: float = 0.0) -> None:
"""Skip backward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
If time_interval is not positive or not present, a default or app-chosen
time interval is used, which is typically 10, 15, 30, etc. seconds.
"""
return await self.relay("skip_backward")()
return await self.relay("skip_backward")(time_interval)

@shield.guard
async def set_position(self, pos: int) -> None:
Expand Down
12 changes: 7 additions & 5 deletions pyatv/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,18 +407,20 @@ async def wakeup(self) -> None:
"SkipForward",
"Skip forward a time interval.",
)
async def skip_forward(self) -> None:
async def skip_forward(self, time_interval: float = 0.0) -> None:
"""Skip forward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
If time_interval is not positive or not present, a default or app-chosen
time interval is used, which is typically 10, 15, 30, etc. seconds.
"""
raise exceptions.NotSupportedError()

@feature(37, "SkipBackward", "Skip backwards a time interval.")
async def skip_backward(self) -> None:
"""Skip backwards a time interval.
async def skip_backward(self, time_interval: float = 0.0) -> None:
"""Skip backward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
If time_interval is not positive or not present, a default or app-chosen
time interval is used, which is typically 10, 15, 30, etc. seconds.
"""
raise exceptions.NotSupportedError()

Expand Down
17 changes: 17 additions & 0 deletions pyatv/protocols/companion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
# that pyatv supports).
PAIRING_WITH_PIN_SUPPORTED_MASK = 0x4000

_DEFAULT_SKIP_TIME = 10
postlund marked this conversation as resolved.
Show resolved Hide resolved

# pylint: disable=invalid-name


Expand Down Expand Up @@ -339,6 +341,21 @@ async def previous(self) -> None:
"""Press key previous."""
await self.api.mediacontrol_command(MediaControlCommand.PreviousTrack)

async def skip_forward(self, time_interval: float = 0.0) -> None:
await self.api.mediacontrol_command(
MediaControlCommand.SkipBy,
{"_skpS": float(time_interval if time_interval > 0 else _DEFAULT_SKIP_TIME)}
)

async def skip_backward(self, time_interval: float = 0.0) -> None:
# float cast: opack fails with negative integers
await self.api.mediacontrol_command(
MediaControlCommand.SkipBy,
{ "_skpS":
float(-time_interval if time_interval > 0 else -_DEFAULT_SKIP_TIME)
}
)

async def channel_up(self) -> None:
"""Select next channel."""
await self._press_button(HidCommand.ChannelIncrement)
Expand Down
10 changes: 6 additions & 4 deletions pyatv/protocols/dmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,23 +353,25 @@ async def volume_down(self) -> None:
"""Press key volume down."""
await self.apple_tv.ctrl_int_cmd("volumedown")

async def skip_forward(self) -> None:
async def skip_forward(self, time_interval: float = 0.0) -> None:
"""Skip forward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
"""
current_position = (await self.apple_tv.playstatus()).position
if current_position:
await self.set_position(current_position + _DEFAULT_SKIP_TIME)
await self.set_position(current_position +
(int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME))

async def skip_backward(self) -> None:
async def skip_backward(self, time_interval: float = 0.0) -> None:
"""Skip backwards a time interval.

Skip interval is typically 15-30s, but is decided by the app.
"""
current_position = (await self.apple_tv.playstatus()).position
if current_position:
await self.set_position(current_position - _DEFAULT_SKIP_TIME)
await self.set_position(current_position -
(int(time_interval) if time_interval > 0 else _DEFAULT_SKIP_TIME))

async def set_position(self, pos: int) -> None:
"""Seek in the current playing media."""
Expand Down
19 changes: 12 additions & 7 deletions pyatv/protocols/mrp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@

_LOGGER = logging.getLogger(__name__)

_DEFAULT_SKIP_TIME = 15

# Source: https://github.com/Daij-Djan/DDHidLib/blob/master/usb_hid_usages.txt
_KEY_LOOKUP = {
# name: [usage_page, usage]
Expand Down Expand Up @@ -429,28 +431,31 @@ async def wakeup(self) -> None:
"""Wake up the device."""
await _send_hid_key(self.protocol, "wakeup", InputAction.SingleTap)

async def skip_forward(self) -> None:
async def skip_forward(self, time_interval: float = 0.0) -> None:
"""Skip forward a time interval.

Skip interval is typically 15-30s, but is decided by the app.
"""
await self._skip_command(CommandInfo_pb2.SkipForward)
await self._skip_command(CommandInfo_pb2.SkipForward, time_interval)

async def skip_backward(self) -> None:
async def skip_backward(self, time_interval: float = 0.0) -> None:
"""Skip backwards a time interval.

Skip interval is typically 15-30s, but is decided by the app.
"""
await self._skip_command(CommandInfo_pb2.SkipBackward)
await self._skip_command(CommandInfo_pb2.SkipBackward, time_interval)

async def _skip_command(self, command) -> None:
async def _skip_command(self, command, time_interval: float) -> None:
info = self.psm.playing.command_info(command)

skip_interval: int
if time_interval > 0:
skip_interval = int(time_interval)
# Pick the first preferred interval for simplicity
if info and info.preferredIntervals:
elif info and info.preferredIntervals:
skip_interval = info.preferredIntervals[0]
else:
skip_interval = 15 # Default value
skip_interval = _DEFAULT_SKIP_TIME # Default value

await self._send_command(command, skipInterval=skip_interval)

Expand Down
7 changes: 6 additions & 1 deletion tests/fake_device/companion.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

DEVICE_NAME = "Fake Companion ATV"
INITIAL_VOLUME = 10.0
INITIAL_DURATION = 10.0
VOLUME_STEP = 5.0
INITIAL_RTI_TEXT = "Fake Companion Keyboard Text"

Expand Down Expand Up @@ -55,6 +56,7 @@
MediaControlCommand.NextTrack: "next",
MediaControlCommand.PreviousTrack: "previous",
MediaControlCommand.SetVolume: "set_volume",
MediaControlCommand.SkipBy: "skip",
}


Expand Down Expand Up @@ -97,6 +99,7 @@ def __init__(self):
self.media_control_flags: int = MediaControlFlags.Volume
self.interests: Set[str] = set()
self.volume: float = INITIAL_VOLUME
self.duration: float = INITIAL_DURATION
self.rti_clients: List[FakeCompanionService] = []
self._rti_focus_state: KeyboardFocusState = KeyboardFocusState.Focused
self.rti_text: Optional[str] = INITIAL_RTI_TEXT
Expand Down Expand Up @@ -454,8 +457,10 @@ def handle__mcc(self, message):
if mcc == MediaControlCommand.SetVolume:
# Make sure we send response before triggering event with volume update
self.loop.call_soon(self.volume_changed(message["_c"]["_vol"] * 100.0))
if mcc == MediaControlCommand.GetVolume:
elif mcc == MediaControlCommand.GetVolume:
args["_vol"] = self.state.volume / 100.0
elif mcc == MediaControlCommand.SkipBy:
self.state.duration = max(0, self.state.duration + message["_c"]["_skpS"])
elif mcc in MEDIA_CONTROL_MAP:
_LOGGER.debug("Activated Media Control Command %s", mcc)
self.state.latest_button = MEDIA_CONTROL_MAP[mcc]
Expand Down
32 changes: 32 additions & 0 deletions tests/protocols/companion/test_companion_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from tests.fake_device.companion import (
INITIAL_RTI_TEXT,
INITIAL_VOLUME,
INITIAL_DURATION,
VOLUME_STEP,
CompanionServiceFlags,
)
Expand Down Expand Up @@ -216,6 +217,37 @@ async def test_remote_control_buttons(companion_client, companion_state, button)
await getattr(companion_client.remote_control, button)()
assert companion_state.latest_button == button

async def test_remote_control_skip_forward_backward(companion_client, companion_state):
duration = companion_state.duration
await companion_client.remote_control.skip_forward()
await until(
lambda: math.isclose(companion_state.duration, duration + 10)
)

duration = companion_state.duration
await companion_client.remote_control.skip_backward()
await until(
lambda: math.isclose(companion_state.duration, duration - 10)
)

duration = companion_state.duration
await companion_client.remote_control.skip_forward(10.5)
await until(
lambda: math.isclose(companion_state.duration, duration + 10.5)
)

duration = companion_state.duration
await companion_client.remote_control.skip_backward(7.3)
await until(
lambda: math.isclose(companion_state.duration, duration - 7.3)
)

# "From beginning"
duration = companion_state.duration
await companion_client.remote_control.skip_backward(9999999.0)
await until(
lambda: math.isclose(companion_state.duration, 0)
)

async def test_audio_set_volume(companion_client, companion_state, companion_usecase):
await until(lambda: companion_client.audio.volume, INITIAL_VOLUME)
Expand Down
10 changes: 10 additions & 0 deletions tests/protocols/dmap/test_dmap_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ async def test_skip_forward_backward(self):
await self.atv.remote_control.skip_backward()
metadata = await self.playing()
self.assertEqual(metadata.position, prev_position - SKIP_TIME)
prev_position = metadata.position

await self.atv.remote_control.skip_forward(13.0)
metadata = await self.playing()
self.assertEqual(metadata.position, prev_position + 13)
prev_position = metadata.position

await self.atv.remote_control.skip_backward(11.0)
metadata = await self.playing()
self.assertEqual(metadata.position, prev_position - 11)

async def test_reset_revision_if_push_updates_fail(self):
"""Test that revision is reset when an error occurs during push update.
Expand Down
7 changes: 7 additions & 0 deletions tests/protocols/mrp/test_mrp_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,13 @@ async def test_skip_forward_backward(self):
self.usecase.change_metadata(title="dummy4")
metadata = await self.playing(title="dummy4")
self.assertEqual(metadata.position, prev_position - 8)
prev_position = metadata.position

# Test specified skip time
await self.atv.remote_control.skip_forward(17)
self.usecase.change_metadata(title="dummy5")
metadata = await self.playing(title="dummy5")
self.assertEqual(metadata.position, prev_position + 17)

async def test_button_play_pause(self):
self.usecase.example_video(supported_commands=[CommandInfo_pb2.TogglePlayPause])
Expand Down
Loading