From f318f5f9b91f581ddf546c773887fc4fb11cd33a Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Tue, 11 Nov 2025 21:21:55 -0800 Subject: [PATCH 1/4] feat: rotate video by 90 degrees clockwise --- pyproject.toml | 3 --- ruff.toml | 4 ++++ viewer/media_player.py | 23 +++++++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 ruff.toml diff --git a/pyproject.toml b/pyproject.toml index e4edc4f0e..b597f2f52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,9 +26,6 @@ build-backend = "poetry.core.masonry.api" # Exclude files/directories exclude = ["anthias_app/migrations/*.py"] -# Line length configuration -line-length = 79 - # Python target version target-version = "py39" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..4d829c79a --- /dev/null +++ b/ruff.toml @@ -0,0 +1,4 @@ +line-length = 79 + +[format] +quote-style = "single" diff --git a/viewer/media_player.py b/viewer/media_player.py index 29dc62ef7..b8c957c25 100644 --- a/viewer/media_player.py +++ b/viewer/media_player.py @@ -11,7 +11,7 @@ VIDEO_TIMEOUT = 20 # secs -class MediaPlayer(): +class MediaPlayer: def __init__(self): pass @@ -38,9 +38,15 @@ def set_asset(self, uri, duration): def play(self): self.process = subprocess.Popen( - ['ffplay', '-autoexit', self.uri], + [ + 'ffplay', + '-autoexit', + '-vf', + 'hflip,vflip,rotate=PI/2', + self.uri, + ], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) def stop(self): @@ -84,13 +90,15 @@ def get_alsa_audio_device(self): def __get_options(self): return [ f'--alsa-audio-device={self.get_alsa_audio_device()}', + '--video-filter=rotate{angle=90}', ] def set_asset(self, uri, duration): self.player.set_mrl(uri) settings.load() self.player.audio_output_device_set( - 'alsa', self.get_alsa_audio_device()) + 'alsa', self.get_alsa_audio_device() + ) def play(self): self.player.play() @@ -100,10 +108,13 @@ def stop(self): def is_playing(self): return self.player.get_state() in [ - vlc.State.Playing, vlc.State.Buffering, vlc.State.Opening] + vlc.State.Playing, + vlc.State.Buffering, + vlc.State.Opening, + ] -class MediaPlayerProxy(): +class MediaPlayerProxy: INSTANCE = None @classmethod From 311c9028d4cd80fc07afe748fa3fd22955a96feb Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 13 Nov 2025 12:40:42 -0800 Subject: [PATCH 2/4] feat(frontend): include RotateDisplay component in settings --- static/src/components/settings/index.tsx | 6 ++++ .../components/settings/rotate-display.tsx | 28 +++++++++++++++++++ static/src/store/settings/index.ts | 2 ++ static/src/tests/settings.test.tsx | 2 ++ static/src/tests/utils.ts | 1 + static/src/types.ts | 2 ++ 6 files changed, 41 insertions(+) create mode 100644 static/src/components/settings/rotate-display.tsx diff --git a/static/src/components/settings/index.tsx b/static/src/components/settings/index.tsx index 4f1e5a9b3..ad15a3476 100644 --- a/static/src/components/settings/index.tsx +++ b/static/src/components/settings/index.tsx @@ -17,6 +17,7 @@ import { PlayerName } from '@/components/settings/player-name' import { DefaultDurations } from '@/components/settings/default-durations' import { AudioOutput } from '@/components/settings/audio-output' import { DateFormat } from '@/components/settings/date-format' +import { RotateDisplay } from '@/components/settings/rotate-display' import { ToggleableSetting } from '@/components/settings/toggleable-setting' import { Update } from '@/components/settings/update' @@ -138,6 +139,11 @@ export const Settings = () => { handleInputChange={handleInputChange} /> + + diff --git a/static/src/components/settings/rotate-display.tsx b/static/src/components/settings/rotate-display.tsx new file mode 100644 index 000000000..b2eec5999 --- /dev/null +++ b/static/src/components/settings/rotate-display.tsx @@ -0,0 +1,28 @@ +import { RootState } from '@/types' + +export const RotateDisplay = ({ + settings, + handleInputChange, +}: { + settings: RootState['settings']['settings'] + handleInputChange: (e: React.ChangeEvent) => void +}) => { + return ( +
+ + +
+ ) +} diff --git a/static/src/store/settings/index.ts b/static/src/store/settings/index.ts index ee97c67e4..328c258ac 100644 --- a/static/src/store/settings/index.ts +++ b/static/src/store/settings/index.ts @@ -76,6 +76,7 @@ export const updateSettings = createAsyncThunk( shuffle_playlist: settings.shufflePlaylist, use_24_hour_clock: settings.use24HourClock, debug_logging: settings.debugLogging, + rotate_display: settings.rotateDisplay, }), }) @@ -176,6 +177,7 @@ const initialState = { shufflePlaylist: false, use24HourClock: false, debugLogging: false, + rotateDisplay: 0, }, deviceModel: '', prevAuthBackend: '', diff --git a/static/src/tests/settings.test.tsx b/static/src/tests/settings.test.tsx index 5313aceb9..fe54d66de 100644 --- a/static/src/tests/settings.test.tsx +++ b/static/src/tests/settings.test.tsx @@ -45,6 +45,7 @@ const createMockStore = (preloadedState: Partial = {}) => { shufflePlaylist: true, use24HourClock: false, debugLogging: true, + rotateDisplay: 0, }, deviceModel: 'Raspberry Pi 4', isLoading: false, @@ -180,6 +181,7 @@ describe('Settings Component', () => { shufflePlaylist: true, use24HourClock: false, debugLogging: true, + rotateDisplay: 0, }, deviceModel: 'Raspberry Pi 4', isLoading: true, diff --git a/static/src/tests/utils.ts b/static/src/tests/utils.ts index 621785daf..d1ac77ec7 100644 --- a/static/src/tests/utils.ts +++ b/static/src/tests/utils.ts @@ -154,6 +154,7 @@ export function getInitialState(): RootState { shufflePlaylist: false, use24HourClock: false, debugLogging: false, + rotateDisplay: 0, }, deviceModel: '', prevAuthBackend: '', diff --git a/static/src/types.ts b/static/src/types.ts index c4e74c016..0566728c5 100644 --- a/static/src/types.ts +++ b/static/src/types.ts @@ -117,6 +117,7 @@ export interface RootState { shufflePlaylist: boolean use24HourClock: boolean debugLogging: boolean + rotateDisplay: number } deviceModel: string prevAuthBackend: string @@ -174,6 +175,7 @@ export interface SettingsData { shufflePlaylist: boolean use24HourClock: boolean debugLogging: boolean + rotateDisplay: number } export interface SystemOperationParams { From 257e16bd932fff0bbdaa3bd5912b22fc014f12ba Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 13 Nov 2025 14:27:06 -0800 Subject: [PATCH 3/4] feat(api): add content rotation support with API endpoints and settings --- api/serializers/v2.py | 2 ++ api/tests/test_v2_endpoints.py | 2 ++ api/views/v2.py | 3 +++ settings.py | 1 + static/src/components/settings/rotate-display.tsx | 2 +- static/src/store/settings/index.ts | 1 + 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api/serializers/v2.py b/api/serializers/v2.py index a2087a213..ef907818d 100644 --- a/api/serializers/v2.py +++ b/api/serializers/v2.py @@ -85,6 +85,7 @@ class DeviceSettingsSerializerV2(Serializer): shuffle_playlist = BooleanField() use_24_hour_clock = BooleanField() debug_logging = BooleanField() + rotate_display = IntegerField() username = CharField() @@ -99,6 +100,7 @@ class UpdateDeviceSettingsSerializerV2(Serializer): shuffle_playlist = BooleanField(required=False) use_24_hour_clock = BooleanField(required=False) debug_logging = BooleanField(required=False) + rotate_display = IntegerField(required=False) username = CharField(required=False, allow_blank=True) password = CharField(required=False, allow_blank=True) password_2 = CharField(required=False, allow_blank=True) diff --git a/api/tests/test_v2_endpoints.py b/api/tests/test_v2_endpoints.py index 1680bb1e0..6496e357e 100644 --- a/api/tests/test_v2_endpoints.py +++ b/api/tests/test_v2_endpoints.py @@ -31,6 +31,7 @@ def test_get_device_settings(self, settings_mock): 'shuffle_playlist': False, 'use_24_hour_clock': True, 'debug_logging': False, + 'rotate_display': 0, 'user': '', }[key] @@ -246,6 +247,7 @@ def test_disable_basic_auth(self, publisher_mock, settings_mock): 'shuffle_playlist': False, 'use_24_hour_clock': True, 'debug_logging': False, + 'rotate_display': 0, }[key] settings_mock.__setitem__ = mock.MagicMock() settings_mock.auth_backends = { diff --git a/api/views/v2.py b/api/views/v2.py index 61dcd3c83..d8a98ac85 100644 --- a/api/views/v2.py +++ b/api/views/v2.py @@ -218,6 +218,7 @@ def get(self, request): 'shuffle_playlist': settings['shuffle_playlist'], 'use_24_hour_clock': settings['use_24_hour_clock'], 'debug_logging': settings['debug_logging'], + 'rotate_display': int(settings['rotate_display']), 'username': ( settings['user'] if settings['auth_backend'] == 'auth_basic' @@ -354,6 +355,8 @@ def patch(self, request): settings['use_24_hour_clock'] = data['use_24_hour_clock'] if 'debug_logging' in data: settings['debug_logging'] = data['debug_logging'] + if 'rotate_display' in data: + settings['rotate_display'] = data['rotate_display'] settings.save() publisher = ZmqPublisher.get_instance() diff --git a/settings.py b/settings.py index b848ee6d4..809ba2a78 100644 --- a/settings.py +++ b/settings.py @@ -37,6 +37,7 @@ 'default_streaming_duration': '300', 'player_name': '', 'resolution': '1920x1080', + 'rotate_display': 0, 'show_splash': True, 'shuffle_playlist': False, 'verify_ssl': True, diff --git a/static/src/components/settings/rotate-display.tsx b/static/src/components/settings/rotate-display.tsx index b2eec5999..010b92cc8 100644 --- a/static/src/components/settings/rotate-display.tsx +++ b/static/src/components/settings/rotate-display.tsx @@ -15,7 +15,7 @@ export const RotateDisplay = ({