Repository: zauberzeug/nicegui
Pull request #2449 (2024-01-27 14:33:00)

Let ui.audio and ui.video inherit from SourceElement
This PR solves feature request #2444 by deriving `ui.audio` and `ui.video` from `SourceElement`, which has a bindable source and serves local files automatically.


Patch:
diff --git a/nicegui/elements/audio.py b/nicegui/elements/audio.py
index d14f0754..7c2c03e0 100644
--- a/nicegui/elements/audio.py
+++ b/nicegui/elements/audio.py
@@ -1,11 +1,11 @@
 from pathlib import Path
 from typing import Union
 
-from .. import core, helpers
-from ..element import Element
+from .mixins.source_element import SourceElement
 
 
-class Audio(Element, component='audio.js'):
+class Audio(SourceElement, component='audio.js'):
+    SOURCE_IS_MEDIA_FILE = True
 
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
@@ -26,10 +26,7 @@ class Audio(Element, component='audio.js'):
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#events>`_
         for a list of events you can subscribe to using the generic event subscription `on()`.
         """
-        super().__init__()
-        if helpers.is_file(src):
-            src = core.app.add_media_file(local_file=src)
-        self._props['src'] = src
+        super().__init__(source=src)
         self._props['controls'] = controls
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
diff --git a/nicegui/elements/mixins/source_element.py b/nicegui/elements/mixins/source_element.py
index e6294583..d453ac68 100644
--- a/nicegui/elements/mixins/source_element.py
+++ b/nicegui/elements/mixins/source_element.py
@@ -13,6 +13,8 @@ class SourceElement(Element):
     source = BindableProperty(
         on_change=lambda sender, source: cast(Self, sender)._handle_source_change(source))  # pylint: disable=protected-access
 
+    SOURCE_IS_MEDIA_FILE: bool = False
+
     def __init__(self, *, source: Union[str, Path], **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.auto_route: Optional[str] = None
@@ -92,7 +94,10 @@ class SourceElement(Element):
         if is_file(source):
             if self.auto_route:
                 core.app.remove_route(self.auto_route)
-            source = core.app.add_static_file(local_file=source)
+            if self.SOURCE_IS_MEDIA_FILE:
+                source = core.app.add_media_file(local_file=source)
+            else:
+                source = core.app.add_static_file(local_file=source)
             self.auto_route = source
         self._props['src'] = source
 
diff --git a/nicegui/elements/video.py b/nicegui/elements/video.py
index eb9cebff..5b0a14c6 100644
--- a/nicegui/elements/video.py
+++ b/nicegui/elements/video.py
@@ -1,11 +1,11 @@
 from pathlib import Path
 from typing import Union
 
-from .. import core, helpers
-from ..element import Element
+from .mixins.source_element import SourceElement
 
 
-class Video(Element, component='video.js'):
+class Video(SourceElement, component='video.js'):
+    SOURCE_IS_MEDIA_FILE = True
 
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
@@ -26,10 +26,7 @@ class Video(Element, component='video.js'):
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#events>`_
         for a list of events you can subscribe to using the generic event subscription `on()`.
         """
-        super().__init__()
-        if helpers.is_file(src):
-            src = core.app.add_media_file(local_file=src)
-        self._props['src'] = src
+        super().__init__(source=src)
         self._props['controls'] = controls
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted


Test patch:
diff --git a/tests/test_audio.py b/tests/test_audio.py
index 423d95ff..7d049c03 100644
--- a/tests/test_audio.py
+++ b/tests/test_audio.py
@@ -1,15 +1,18 @@
 from nicegui import ui
 from nicegui.testing import Screen
 
+SONG1 = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
+SONG2 = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'
+
 
 def test_replace_audio(screen: Screen):
     with ui.row() as container:
-        ui.audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3')
+        ui.audio(SONG1)
 
     def replace():
         container.clear()
         with container:
-            ui.audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3')
+            ui.audio(SONG2)
     ui.button('Replace', on_click=replace)
 
     screen.open('/')
@@ -17,3 +20,14 @@ def test_replace_audio(screen: Screen):
     screen.click('Replace')
     screen.wait(0.5)
     assert screen.find_by_tag('audio').get_attribute('src').endswith('SoundHelix-Song-2.mp3')
+
+
+def test_change_source(screen: Screen):
+    audio = ui.audio(SONG1)
+    ui.button('Change source', on_click=lambda: audio.set_source(SONG2))
+
+    screen.open('/')
+    assert screen.find_by_tag('audio').get_attribute('src').endswith('SoundHelix-Song-1.mp3')
+    screen.click('Change source')
+    screen.wait(0.5)
+    assert screen.find_by_tag('audio').get_attribute('src').endswith('SoundHelix-Song-2.mp3')
diff --git a/tests/test_video.py b/tests/test_video.py
index dbf5b99c..3c45995f 100644
--- a/tests/test_video.py
+++ b/tests/test_video.py
@@ -1,15 +1,18 @@
 from nicegui import ui
 from nicegui.testing import Screen
 
+VIDEO1 = 'https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
+VIDEO2 = 'https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4'
+
 
 def test_replace_video(screen: Screen):
     with ui.row() as container:
-        ui.video('https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4')
+        ui.video(VIDEO1)
 
     def replace():
         container.clear()
         with container:
-            ui.video('https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4')
+            ui.video(VIDEO2)
     ui.button('Replace', on_click=replace)
 
     screen.open('/')
@@ -17,3 +20,14 @@ def test_replace_video(screen: Screen):
     screen.click('Replace')
     screen.wait(0.5)
     assert screen.find_by_tag('video').get_attribute('src').endswith('ElephantsDream.mp4')
+
+
+def test_change_source(screen: Screen):
+    audio = ui.video(VIDEO1)
+    ui.button('Change source', on_click=lambda: audio.set_source(VIDEO2))
+
+    screen.open('/')
+    assert screen.find_by_tag('video').get_attribute('src').endswith('BigBuckBunny.mp4')
+    screen.click('Change source')
+    screen.wait(0.5)
+    assert screen.find_by_tag('video').get_attribute('src').endswith('ElephantsDream.mp4')