Refresh env, add aiohttp dep, improve resource selection and GStreamer flags
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "/home/enne2/.conda/envs/r36s-dlna-browser/bin/python"
|
||||||
|
}
|
||||||
@@ -90,15 +90,20 @@ During playback, the same controls are remapped to transport actions:
|
|||||||
|
|
||||||
## On-Device (R36S / ArkOS)
|
## On-Device (R36S / ArkOS)
|
||||||
|
|
||||||
1. Copy the project to `/roms/ports/` or another writable location.
|
1. Copy the project to a writable location such as `/home/ark/MatHacks/R36SHack`.
|
||||||
2. Ensure Wi-Fi is connected.
|
2. Ensure Wi-Fi is connected.
|
||||||
3. Run via PortMaster or a launch script:
|
3. Install or reuse a conda env at `/home/ark/miniconda3/envs/r36s-dlna-browser`.
|
||||||
|
4. Run via PortMaster or a launch script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /roms/ports/R36SHack
|
cd /home/ark/MatHacks/R36SHack
|
||||||
PYTHONPATH=src python3 -m r36s_dlna_browser
|
export LD_LIBRARY_PATH=/home/ark/miniconda3/envs/r36s-dlna-browser/lib
|
||||||
|
export GST_PLUGIN_SCANNER=/home/ark/miniconda3/envs/r36s-dlna-browser/libexec/gstreamer-1.0/gst-plugin-scanner
|
||||||
|
python -m r36s_dlna_browser
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An ArkOS launcher script for this layout is included at `deploy/arkos/MatHacks.sh`.
|
||||||
|
|
||||||
## Git / Release Workflow
|
## Git / Release Workflow
|
||||||
|
|
||||||
For publication on a Gitea instance with `tea`, initialize git locally, create a
|
For publication on a Gitea instance with `tea`, initialize git locally, create a
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Milestone 3 — SDL Video Viewport, HUD, and Wayland Compatibility
|
|||||||
- **Playback HUD**: SDL-rendered overlay is now simplified and compact, uses bundled playback icons, uses a smaller dedicated playback font with title ellipsis for 640x480 readability, supports auto-hide or fixed visibility, stays visible while playback is paused, and remains in the same SDL render pass as video
|
- **Playback HUD**: SDL-rendered overlay is now simplified and compact, uses bundled playback icons, uses a smaller dedicated playback font with title ellipsis for 640x480 readability, supports auto-hide or fixed visibility, stays visible while playback is paused, and remains in the same SDL render pass as video
|
||||||
- **Wayland / DRM strategy**: playback no longer depends on native overlay sinks or X11 window handles; R36S-class targets continue to prefer `kmsdrm` when no display server is present
|
- **Wayland / DRM strategy**: playback no longer depends on native overlay sinks or X11 window handles; R36S-class targets continue to prefer `kmsdrm` when no display server is present
|
||||||
- **Deploy packaging**: a `conda-forge`-oriented `environment.yml` now defines a reproducible Miniforge/Miniconda environment for local development and release preparation
|
- **Deploy packaging**: a `conda-forge`-oriented `environment.yml` now defines a reproducible Miniforge/Miniconda environment for local development and release preparation
|
||||||
|
- **Python packaging**: direct runtime dependency declarations now explicitly include `aiohttp` instead of relying on transitive installation through other packages
|
||||||
|
- **ArkOS deploy layout**: on-device installs should place Miniforge and the git checkout under `/home/ark` to avoid the full `/roms` partition, while EmulationStation integration should stay lightweight under `/roms/ports`
|
||||||
|
|
||||||
## Completed Tasks
|
## Completed Tasks
|
||||||
|
|
||||||
@@ -38,12 +40,19 @@ Milestone 3 — SDL Video Viewport, HUD, and Wayland Compatibility
|
|||||||
- Playback-page flashing root cause addressed by removing native overlay composition entirely: video and HUD are now rendered together by SDL in one pass, with redraws driven by decoded frame availability and HUD state changes
|
- Playback-page flashing root cause addressed by removing native overlay composition entirely: video and HUD are now rendered together by SDL in one pass, with redraws driven by decoded frame availability and HUD state changes
|
||||||
- Playback HUD simplified: the border around the video area was removed, playback control/status icons were added as bundled SVG+PNG assets, the title/timer top bar no longer overlaps, and playback now supports `auto / fixed / hidden` HUD modes through a dedicated command while staying visible when paused
|
- Playback HUD simplified: the border around the video area was removed, playback control/status icons were added as bundled SVG+PNG assets, the title/timer top bar no longer overlaps, and playback now supports `auto / fixed / hidden` HUD modes through a dedicated command while staying visible when paused
|
||||||
- Deployment assets added: `.gitignore`, `environment.yml`, and a real `LICENSE` file so the project can be initialized and published as a clean git repository
|
- Deployment assets added: `.gitignore`, `environment.yml`, and a real `LICENSE` file so the project can be initialized and published as a clean git repository
|
||||||
|
- Conda environment refreshed for current playback needs: runtime now includes GStreamer codec/plugin packages plus explicit Python build/test tooling, while editable install keeps the package code sourced from the repo checkout
|
||||||
|
- Packaging fix: `pyproject.toml` now uses a valid TOML `[project.urls]` table so editable installs work with modern `pip` / `tomllib`
|
||||||
- Copilot instructions and this status file
|
- Copilot instructions and this status file
|
||||||
|
- Device deployment reconnaissance completed on a real ArkOS-derived R36S over SSH: `/roms` is full, `/home/ark` has free space, required download tools are present, and `/roms/ports` plus `gamelist.xml` are the least invasive integration points for launchers
|
||||||
|
|
||||||
## Tasks In Progress
|
## Tasks In Progress
|
||||||
|
|
||||||
- Verify that the SDL-texture playback path is smooth enough on real host playback and on R36S hardware
|
- Verify that the SDL-texture playback path is smooth enough on real host playback and on R36S hardware
|
||||||
- Measure whether BGRA frame upload is acceptable on RK3326 or whether a future YUV texture path is needed
|
- Measure whether BGRA frame upload is acceptable on RK3326 or whether a future YUV texture path is needed
|
||||||
|
- Validate real video playback on the physical R36S after adding the missing H.264 and AAC decoder plugins to the device conda env
|
||||||
|
- Device deployment on the physical R36S is now wired through ArkOS `Ports -> MatHacks`, with the heavy runtime under `/home/ark` and only a lightweight stub launcher under `/roms/ports`
|
||||||
|
- Device env bootstrap on the physical R36S reaches a clean `from r36s_dlna_browser.app import Application` inside `/home/ark/miniconda3/envs/r36s-dlna-browser`
|
||||||
|
- ArkOS launcher asset added at `deploy/arkos/MatHacks.sh` with the verified `LD_LIBRARY_PATH`, `GST_PLUGIN_SCANNER`, and `GST_PLUGIN_SYSTEM_PATH_1_0` exports needed by the conda runtime
|
||||||
|
|
||||||
## Blockers Or Open Questions
|
## Blockers Or Open Questions
|
||||||
|
|
||||||
@@ -52,10 +61,20 @@ Milestone 3 — SDL Video Viewport, HUD, and Wayland Compatibility
|
|||||||
- Root browse verified against two real DLNA servers on the LAN.
|
- Root browse verified against two real DLNA servers on the LAN.
|
||||||
- On-device testing on R36S hardware is pending.
|
- On-device testing on R36S hardware is pending.
|
||||||
- The current SDL-texture path avoids window-manager dependencies but may still need optimization on low-end hardware if BGRA upload cost is too high.
|
- The current SDL-texture path avoids window-manager dependencies but may still need optimization on low-end hardware if BGRA upload cost is too high.
|
||||||
|
- The first Miniforge install attempt on the physical R36S failed because the downloaded installer was corrupt and crashed during extraction.
|
||||||
|
- The physical R36S now has Miniconda installed at `/home/ark/miniconda3`; the dedicated app env exists at `/home/ark/miniconda3/envs/r36s-dlna-browser`, but package solves can hang on-device and are being handled incrementally.
|
||||||
|
- The dedicated R36S conda env requires `LD_LIBRARY_PATH=/home/ark/miniconda3/envs/r36s-dlna-browser/lib` for GI and GStreamer shared libraries to resolve correctly.
|
||||||
|
- GStreamer imports now succeed in the dedicated env (`GLib`, `GObject`, `Gst`, `GstApp`, `GstVideo`), and `Application` imports cleanly.
|
||||||
|
- ArkOS menu launch works on the physical device, and DLNA browsing reaches real MiniDLNA content.
|
||||||
|
- Real playback is currently blocked by missing decoder elements in the device env: direct probing of a MiniDLNA `.mkv` URL showed missing H.264 High Profile and MPEG-4 AAC decoders, while the user-facing "can't play a text file" message is a misleading fallback caused by an additional text stream in the container.
|
||||||
|
- Direct package installs on the physical R36S now added `gst-libav`, `gst-plugins-bad`, `ffmpeg`, `libxml2-16`, and several related runtime libraries into `/home/ark/miniconda3/envs/r36s-dlna-browser`; `h264parse` now registers, but `gst-libav` still does not expose `avdec_h264` because `libgstlibav.so` remains blocked by unresolved `dav1d` and `icu` ABI/runtime mismatches on `linux-aarch64`.
|
||||||
|
- Some optional GStreamer plugins still warn about additional codec libraries (`libxml2`, `libFLAC`, `libvpx`, `mpg123`) that are not yet installed in the env.
|
||||||
|
|
||||||
## Next Recommended Actions
|
## Next Recommended Actions
|
||||||
|
|
||||||
1. Run a visual playback smoke test and confirm SDL-rendered video plus HUD eliminates flashing on host playback.
|
1. Run a visual playback smoke test and confirm SDL-rendered video plus HUD eliminates flashing on host playback.
|
||||||
2. Validate the SDL-texture playback path on the target R36S `kmsdrm` backend.
|
2. Validate the SDL-texture playback path on the target R36S `kmsdrm` backend.
|
||||||
3. Measure CPU/load on RK3326 hardware during audio and video playback.
|
3. Measure CPU/load on RK3326 hardware during audio and video playback.
|
||||||
4. If RGBA upload cost is too high, add a follow-up YUV texture upload path using `SDL_UpdateYUVTexture`.
|
4. If RGBA upload cost is too high, add a follow-up YUV texture upload path using `SDL_UpdateYUVTexture`.
|
||||||
|
5. Install matching `gst-libav` and `gst-plugins-bad` packages into `/home/ark/miniconda3/envs/r36s-dlna-browser`, then confirm `avdec_h264`, an AAC decoder, and `h264parse` appear in `gst-inspect-1.0`.
|
||||||
|
6. Resolve the remaining `gst-libav` runtime chain on `linux-aarch64` by finding a compatible `ffmpeg`/`dav1d`/`icu` combination or by supplying the missing shared libraries manually, then re-test the real MiniDLNA `.mkv` URL with `gst-launch-1.0` and from ArkOS `Ports -> MatHacks`.
|
||||||
+10
-1
@@ -4,11 +4,20 @@ channels:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- python=3.11
|
- python=3.11
|
||||||
- pip
|
- pip
|
||||||
|
- setuptools>=68
|
||||||
|
- wheel
|
||||||
- pygobject
|
- pygobject
|
||||||
- gstreamer
|
- gstreamer
|
||||||
- gst-plugins-base
|
- gst-plugins-base
|
||||||
- gst-plugins-good
|
- gst-plugins-good
|
||||||
|
- gst-plugins-bad
|
||||||
|
- gst-libav
|
||||||
|
- ffmpeg
|
||||||
- sdl2
|
- sdl2
|
||||||
- sdl2_ttf
|
- sdl2_ttf
|
||||||
|
- cairo
|
||||||
|
- pkg-config
|
||||||
|
- pytest>=7
|
||||||
|
- pytest-asyncio>=0.21
|
||||||
- pip:
|
- pip:
|
||||||
- -e .[dev]
|
- -e .
|
||||||
|
|||||||
+5
-4
@@ -12,15 +12,16 @@ license = "MIT"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "Matteo Benedetto"},
|
{name = "Matteo Benedetto"},
|
||||||
]
|
]
|
||||||
urls = {
|
|
||||||
Homepage = "https://git.enne2.net/enne2/R36SHack",
|
|
||||||
Repository = "https://git.enne2.net/enne2/R36SHack",
|
|
||||||
}
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"PySDL2>=0.9.16",
|
"PySDL2>=0.9.16",
|
||||||
|
"aiohttp>=3.9",
|
||||||
"async-upnp-client>=0.38.0",
|
"async-upnp-client>=0.38.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://git.enne2.net/enne2/R36SHack"
|
||||||
|
Repository = "https://git.enne2.net/enne2/R36SHack"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.0",
|
"pytest>=7.0",
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
aiohttp>=3.9
|
||||||
PySDL2>=0.9.16
|
PySDL2>=0.9.16
|
||||||
async-upnp-client>=0.38.0
|
async-upnp-client>=0.38.0
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from xml.etree import ElementTree
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from r36s_dlna_browser.dlna.models import MediaItem, ItemType, classify_upnp_class
|
from r36s_dlna_browser.dlna.models import MediaItem, ItemType, classify_upnp_class, select_best_resource
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -103,21 +103,27 @@ def _parse_didl(didl_xml: str, base_url: str) -> List[MediaItem]:
|
|||||||
parent_id = item_el.get("parentID", "0")
|
parent_id = item_el.get("parentID", "0")
|
||||||
upnp_class = _text(item_el.find("upnp:class", _NS))
|
upnp_class = _text(item_el.find("upnp:class", _NS))
|
||||||
album_art = _text(item_el.find("upnp:albumArtURI", _NS))
|
album_art = _text(item_el.find("upnp:albumArtURI", _NS))
|
||||||
|
item_type = classify_upnp_class(upnp_class)
|
||||||
|
|
||||||
resource_url = ""
|
resource_url = ""
|
||||||
mime_type = ""
|
mime_type = ""
|
||||||
duration = ""
|
duration = ""
|
||||||
size = 0
|
size = 0
|
||||||
res_el = item_el.find("didl:res", _NS)
|
resources = []
|
||||||
if res_el is not None:
|
for res_el in item_el.findall("didl:res", _NS):
|
||||||
resource_url = res_el.text.strip() if res_el.text else ""
|
resources.append({
|
||||||
protocol_info = res_el.get("protocolInfo", "")
|
"#text": res_el.text.strip() if res_el.text else "",
|
||||||
# Extract mime from protocolInfo: "http-get:*:audio/mpeg:*"
|
"@protocolInfo": res_el.get("protocolInfo", ""),
|
||||||
parts = protocol_info.split(":")
|
"@duration": res_el.get("duration", ""),
|
||||||
if len(parts) >= 3:
|
"@size": res_el.get("size", "0"),
|
||||||
mime_type = parts[2]
|
})
|
||||||
duration = res_el.get("duration", "")
|
|
||||||
size = int(res_el.get("size", "0") or "0")
|
selected_resource = select_best_resource(resources, item_type)
|
||||||
|
if selected_resource is not None:
|
||||||
|
resource_url = str(selected_resource["url"])
|
||||||
|
mime_type = str(selected_resource["mime_type"])
|
||||||
|
duration = str(selected_resource["duration"])
|
||||||
|
size = int(selected_resource["size"])
|
||||||
|
|
||||||
# Resolve relative URLs
|
# Resolve relative URLs
|
||||||
if resource_url and not resource_url.startswith(("http://", "https://")):
|
if resource_url and not resource_url.startswith(("http://", "https://")):
|
||||||
@@ -128,7 +134,7 @@ def _parse_didl(didl_xml: str, base_url: str) -> List[MediaItem]:
|
|||||||
items.append(MediaItem(
|
items.append(MediaItem(
|
||||||
object_id=obj_id,
|
object_id=obj_id,
|
||||||
title=title,
|
title=title,
|
||||||
item_type=classify_upnp_class(upnp_class),
|
item_type=item_type,
|
||||||
parent_id=parent_id,
|
parent_id=parent_id,
|
||||||
resource_url=resource_url,
|
resource_url=resource_url,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
class ItemType(Enum):
|
class ItemType(Enum):
|
||||||
@@ -49,6 +51,116 @@ class MediaItem:
|
|||||||
return self.title or self.object_id
|
return self.title or self.object_id
|
||||||
|
|
||||||
|
|
||||||
|
_SUBTITLE_EXTENSIONS = {".ass", ".idx", ".srt", ".ssa", ".sub", ".sup", ".ttml", ".vtt"}
|
||||||
|
_VIDEO_EXTENSIONS = {".avi", ".m2ts", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", ".ts", ".webm"}
|
||||||
|
_AUDIO_EXTENSIONS = {".aac", ".flac", ".m4a", ".mp3", ".ogg", ".opus", ".wav"}
|
||||||
|
_IMAGE_EXTENSIONS = {".bmp", ".gif", ".jpeg", ".jpg", ".png", ".webp"}
|
||||||
|
_SUBTITLE_MIME_MARKERS = (
|
||||||
|
"application/ass",
|
||||||
|
"application/pgs",
|
||||||
|
"application/sdp",
|
||||||
|
"application/smil",
|
||||||
|
"application/srt",
|
||||||
|
"application/ssa",
|
||||||
|
"application/ttml+xml",
|
||||||
|
"application/vtt",
|
||||||
|
"application/x-subrip",
|
||||||
|
"subpicture/",
|
||||||
|
"subtitle/",
|
||||||
|
"text/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_protocol_mime(protocol_info: str) -> str:
|
||||||
|
parts = protocol_info.split(":")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
return parts[2]
|
||||||
|
return protocol_info
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_kind_from_mime(mime_type: str) -> ItemType | str | None:
|
||||||
|
mime = mime_type.lower()
|
||||||
|
if not mime:
|
||||||
|
return None
|
||||||
|
if any(marker in mime for marker in _SUBTITLE_MIME_MARKERS):
|
||||||
|
return "text"
|
||||||
|
if mime.startswith("video/"):
|
||||||
|
return ItemType.VIDEO
|
||||||
|
if mime.startswith("audio/"):
|
||||||
|
return ItemType.AUDIO
|
||||||
|
if mime.startswith("image/"):
|
||||||
|
return ItemType.IMAGE
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_kind_from_url(url: str) -> ItemType | str | None:
|
||||||
|
path = urlparse(url).path.lower()
|
||||||
|
dot = path.rfind(".")
|
||||||
|
ext = path[dot:] if dot >= 0 else ""
|
||||||
|
if ext in _SUBTITLE_EXTENSIONS:
|
||||||
|
return "text"
|
||||||
|
if ext in _VIDEO_EXTENSIONS:
|
||||||
|
return ItemType.VIDEO
|
||||||
|
if ext in _AUDIO_EXTENSIONS:
|
||||||
|
return ItemType.AUDIO
|
||||||
|
if ext in _IMAGE_EXTENSIONS:
|
||||||
|
return ItemType.IMAGE
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_resource(resource: dict[str, Any] | str) -> dict[str, Any]:
|
||||||
|
if isinstance(resource, str):
|
||||||
|
return {
|
||||||
|
"url": resource,
|
||||||
|
"protocol_info": "",
|
||||||
|
"mime_type": "",
|
||||||
|
"size": 0,
|
||||||
|
"duration": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol_info = str(resource.get("protocol_info", resource.get("@protocolInfo", "")))
|
||||||
|
mime_type = str(resource.get("mime_type", "")) or _extract_protocol_mime(protocol_info)
|
||||||
|
return {
|
||||||
|
"url": str(resource.get("url", resource.get("uri", resource.get("#text", "")))),
|
||||||
|
"protocol_info": protocol_info,
|
||||||
|
"mime_type": mime_type,
|
||||||
|
"size": int(resource.get("size", resource.get("@size", 0)) or 0),
|
||||||
|
"duration": str(resource.get("duration", resource.get("@duration", ""))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def select_best_resource(resources: Sequence[dict[str, Any] | str], item_type: ItemType) -> dict[str, Any] | None:
|
||||||
|
best: dict[str, Any] | None = None
|
||||||
|
best_key: tuple[int, int, int] | None = None
|
||||||
|
|
||||||
|
for resource in resources:
|
||||||
|
normalized = _normalize_resource(resource)
|
||||||
|
url = normalized["url"]
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
resource_kind = _resource_kind_from_mime(normalized["mime_type"]) or _resource_kind_from_url(url)
|
||||||
|
score = 0
|
||||||
|
if item_type != ItemType.UNKNOWN and resource_kind == item_type:
|
||||||
|
score += 10
|
||||||
|
elif resource_kind == "text":
|
||||||
|
score -= 20
|
||||||
|
elif item_type != ItemType.UNKNOWN and resource_kind is not None:
|
||||||
|
score -= 4
|
||||||
|
elif resource_kind in {ItemType.AUDIO, ItemType.VIDEO, ItemType.IMAGE}:
|
||||||
|
score += 3
|
||||||
|
|
||||||
|
if normalized["protocol_info"].startswith("http-get:"):
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
key = (score, int(normalized["size"]), len(url))
|
||||||
|
if best_key is None or key > best_key:
|
||||||
|
best = normalized
|
||||||
|
best_key = key
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
def classify_upnp_class(upnp_class: str) -> ItemType:
|
def classify_upnp_class(upnp_class: str) -> ItemType:
|
||||||
"""Map a UPnP class string to our ItemType enum."""
|
"""Map a UPnP class string to our ItemType enum."""
|
||||||
c = upnp_class.lower()
|
c = upnp_class.lower()
|
||||||
@@ -79,15 +191,12 @@ def parse_didl_item(didl_item: dict) -> MediaItem:
|
|||||||
resources = didl_item.get("resources", didl_item.get("res", []))
|
resources = didl_item.get("resources", didl_item.get("res", []))
|
||||||
if isinstance(resources, dict):
|
if isinstance(resources, dict):
|
||||||
resources = [resources]
|
resources = [resources]
|
||||||
if resources:
|
selected_resource = select_best_resource(resources, item_type)
|
||||||
res = resources[0]
|
if selected_resource is not None:
|
||||||
if isinstance(res, dict):
|
resource_url = str(selected_resource["url"])
|
||||||
resource_url = res.get("url", res.get("uri", res.get("#text", "")))
|
mime_type = str(selected_resource["protocol_info"] or selected_resource["mime_type"])
|
||||||
mime_type = res.get("protocol_info", res.get("@protocolInfo", ""))
|
size = int(selected_resource["size"])
|
||||||
size = int(res.get("size", res.get("@size", 0)) or 0)
|
duration = str(selected_resource["duration"])
|
||||||
duration = res.get("duration", res.get("@duration", ""))
|
|
||||||
elif isinstance(res, str):
|
|
||||||
resource_url = res
|
|
||||||
|
|
||||||
album_art = didl_item.get("album_art_uri", didl_item.get("albumArtURI", ""))
|
album_art = didl_item.get("album_art_uri", didl_item.get("albumArtURI", ""))
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ class GStreamerBackend(PlayerBackend):
|
|||||||
return self._pipeline
|
return self._pipeline
|
||||||
|
|
||||||
self._pipeline = self._playbin_factory()
|
self._pipeline = self._playbin_factory()
|
||||||
|
self._configure_playbin_flags(self._pipeline)
|
||||||
self._video_sink = self._create_appsink()
|
self._video_sink = self._create_appsink()
|
||||||
self._pipeline.set_property("video-sink", self._video_sink)
|
self._pipeline.set_property("video-sink", self._video_sink)
|
||||||
self._pipeline.set_property("volume", self._volume / 100.0)
|
self._pipeline.set_property("volume", self._volume / 100.0)
|
||||||
@@ -267,6 +268,24 @@ class GStreamerBackend(PlayerBackend):
|
|||||||
self._start_bus_thread()
|
self._start_bus_thread()
|
||||||
return self._pipeline
|
return self._pipeline
|
||||||
|
|
||||||
|
def _configure_playbin_flags(self, pipeline) -> None:
|
||||||
|
play_flags = getattr(self._gst, "PlayFlags", None)
|
||||||
|
if play_flags is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
flags = int(pipeline.get_property("flags"))
|
||||||
|
for required in ("AUDIO", "VIDEO"):
|
||||||
|
value = getattr(play_flags, required, None)
|
||||||
|
if value is not None:
|
||||||
|
flags |= int(value)
|
||||||
|
|
||||||
|
for disabled in ("TEXT", "VIS"):
|
||||||
|
value = getattr(play_flags, disabled, None)
|
||||||
|
if value is not None:
|
||||||
|
flags &= ~int(value)
|
||||||
|
|
||||||
|
pipeline.set_property("flags", flags)
|
||||||
|
|
||||||
def _create_appsink(self):
|
def _create_appsink(self):
|
||||||
sink = self._appsink_factory()
|
sink = self._appsink_factory()
|
||||||
sink.set_property("emit-signals", True)
|
sink.set_property("emit-signals", True)
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ class TestParseDIDL:
|
|||||||
items = _parse_didl(didl, "http://myserver:8200")
|
items = _parse_didl(didl, "http://myserver:8200")
|
||||||
assert items[0].resource_url == "http://myserver:8200/media/track.flac"
|
assert items[0].resource_url == "http://myserver:8200/media/track.flac"
|
||||||
|
|
||||||
|
def test_prefers_video_resource_over_subtitle_resource(self):
|
||||||
|
didl = """\
|
||||||
|
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||||||
|
<item id="400" parentID="1">
|
||||||
|
<dc:title>Movie</dc:title>
|
||||||
|
<upnp:class>object.item.videoItem</upnp:class>
|
||||||
|
<res protocolInfo="http-get:*:text/vtt:*">http://srv/movie.vtt</res>
|
||||||
|
<res protocolInfo="http-get:*:video/x-matroska:*" size="999">http://srv/movie.mkv</res>
|
||||||
|
</item>
|
||||||
|
</DIDL-Lite>
|
||||||
|
"""
|
||||||
|
items = _parse_didl(didl, "http://srv")
|
||||||
|
assert items[0].resource_url == "http://srv/movie.mkv"
|
||||||
|
assert items[0].mime_type == "video/x-matroska"
|
||||||
|
|
||||||
|
|
||||||
class TestExtractBrowseResult:
|
class TestExtractBrowseResult:
|
||||||
def test_extracts_unnamespaced_result(self):
|
def test_extracts_unnamespaced_result(self):
|
||||||
|
|||||||
@@ -98,6 +98,27 @@ class TestParseDIDLItem:
|
|||||||
item = parse_didl_item(didl)
|
item = parse_didl_item(didl)
|
||||||
assert item.resource_url == "http://srv/vid.mp4"
|
assert item.resource_url == "http://srv/vid.mp4"
|
||||||
|
|
||||||
|
def test_prefers_media_resource_over_subtitle_resource(self):
|
||||||
|
didl = {
|
||||||
|
"id": "3",
|
||||||
|
"title": "Movie",
|
||||||
|
"upnp_class": "object.item.videoItem",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"url": "http://srv/movie.srt",
|
||||||
|
"protocol_info": "http-get:*:text/srt:*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://srv/movie.mkv",
|
||||||
|
"protocol_info": "http-get:*:video/x-matroska:*",
|
||||||
|
"size": "1200",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
item = parse_didl_item(didl)
|
||||||
|
assert item.resource_url == "http://srv/movie.mkv"
|
||||||
|
assert item.mime_type == "http-get:*:video/x-matroska:*"
|
||||||
|
|
||||||
def test_alt_key_names(self):
|
def test_alt_key_names(self):
|
||||||
"""Handles alternative key formats (@id, @parentID, class, etc.)."""
|
"""Handles alternative key formats (@id, @parentID, class, etc.)."""
|
||||||
didl = {
|
didl = {
|
||||||
|
|||||||
Reference in New Issue
Block a user