Fix AirPlay startup and service discovery on Frame

This commit is contained in:
toorux committed 2026-10-26 01:00:39 +08:00
1 parent f7ae2e8e2c
commit fc381c2676
12 files changed
+292 -21

No files matched your search

+2 -2
View File
@@ -59,8 +59,8 @@ jobs:
sudo apt-get update
sudo apt-get install -y g++ cmake pkg-config libgl1-mesa-dev libx11-dev \
libdrm-dev libvulkan-dev glslc libclang-dev libssl-dev libplist-dev \
libavahi-compat-libdnssd-dev libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev libgupnp-1.6-dev \
libavahi-compat-libdnssd-dev libavahi-core-dev libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev libgupnp-1.6-dev libpulse-dev libsoup-3.0-dev \
meson ninja-build libavfilter-dev libavformat-dev libavcodec-dev libavutil-dev
- name: Check release version
env:
+2 -2
View File
@@ -30,7 +30,7 @@ Framecorder 原插件的名称是“面板画面”和“SteamVR 备用画面”
## 接收与发送
AirPlay 由固定版本 UxPlay 处理 RAOP 音频和屏幕镜像;只允许一个发送设备,不实现 AirPlay 发送。自定义 GStreamer sink 在用户确认前丢弃音频和画面。断开、拒绝或停止后清除同意状态,新连接重新询问。
AirPlay 由固定版本 UxPlay 处理 RAOP 音频和屏幕镜像;只允许一个发送设备,不实现 AirPlay 发送。接收进程使用系统 Avahi core 库独立发布 AirPlay/RAOP 服务,不依赖系统 Avahi daemon 的发布权限,也不修改其全局配置。自定义 GStreamer sink 在用户确认前丢弃音频和画面。断开、拒绝或停止后清除同意状态,新连接重新询问。
DLNA 使用 GUPnP 的标准发现、SOAP 和事件订阅,接收 HTTP(S) 媒体地址。每个新请求先显示通知,接受后才下载、解码和播放。接收的音频打开内置控制窗口;视频采用旋转及像素比例校正后的实际画幅,控件浮在视频上并自动收起,不改变窗口画幅。不同 DLNA 请求可以保持多个空间窗口。播放器支持暂停、音量、停止以及媒体支持时的跳转;关闭空间窗口会停止该投屏。未处理请求 60 秒后过期。
@@ -42,7 +42,7 @@ DLNA 发送需要开启网络管理面板并启动串流。GUPnP 发现设备后
头显运行依赖现有 FFmpeg(libx264、libvpx、libopus、AAC、V4L2、PulseAudio)、GStreamer 1.24+、FFmpeg 7 共享库与随包的 gst-libav 插件、GUPnP 1.6、Avahi、OpenVR、Vulkan/DRM、Qualcomm Iris。设备上已确认 FFmpeg、GStreamer 1.24.2、GUPnP 1.6.6 和上述编码器可用。
构建需要 Rust、C/C++ 编译器、CMake、Meson、Ninja、pkg-config、glslc,以及 DRM/Vulkan、GStreamer app/video、GUPnP 1.6、Avahi、OpenSSL/libplist 开发文件。`tools/build-media.sh` 构建采集助手、接收器、gst-libav 插件和 UxPlay,并下载校验固定版本的 MediaMTX。可用 `FRAMELY_SYSROOT` 为采集 crate 指定 ARM64 sysroot;gst-libav 链接 FFmpeg 7 ABI,其余组件使用目标系统的开发库。
构建需要 Rust、C/C++ 编译器、CMake、Meson、Ninja、pkg-config、glslc,以及 DRM/Vulkan、GStreamer app/video、GUPnP 1.6、Avahi、PulseAudio、libsoup、OpenSSL/libplist 开发文件。`tools/build-media.sh` 构建采集助手、接收器、gst-libav、接收管线必需的 gst-plugins-good 插件和 UxPlay,并下载校验固定版本的 MediaMTX。可用 `FRAMELY_SYSROOT` 为采集 crate 指定 ARM64 sysroot;gst-libav 链接 FFmpeg 7 ABI,其余组件使用目标系统的开发库。
`tools/package-release.sh <CEF目录>` 同时构建媒体能力;助手放入 `bin`,其他媒体程序放入 `lib/media`。第三方许可证和媒体改动源码随包提供,版本和 SHA-256 位于 `media/dependencies.json`。
+7 -1
View File
@@ -18,11 +18,17 @@
Its version and checksum are pinned; releases include its source and license.
CI links against the FFmpeg 7 development API to match SteamOS; that build
dependency is pinned but its FFmpeg libraries are not distributed.
- [gst-plugins-good](https://gstreamer.freedesktop.org/modules/gst-plugins-good.html),
LGPL-2.1-or-later, supplies the bundled level, PulseAudio, video filter,
ISO MP4 and HTTP source plugins required by the receive pipelines.
The version and checksum are pinned; source, build recipe and license ship with releases.
- Avahi core (LGPL) is linked as a system library to publish only the receiver’s
AirPlay/RAOP records without changing system Avahi publishing settings.
- GStreamer and GUPnP are linked as system libraries (LGPL). GStreamer performs
decoding, audio playback, rotation and pixel aspect correction; GUPnP provides
UPnP discovery, SOAP and event subscriptions. FFmpeg is invoked as the installed
system executable for SteamVR capture, software encoding and audio muxing.
These system components are not copied into Framely packages.
The GStreamer core, GUPnP and their system libraries are not copied into Framely packages.
- `native/json.hpp` is the existing nlohmann/json dependency (MIT), included with
the main Framely release notices.
+5
View File
@@ -18,5 +18,10 @@
"version": "7.0",
"url": "https://ffmpeg.org/releases/ffmpeg-7.0.tar.xz",
"sha256": "4426a94dd2c814945456600c8adfc402bee65ec14a70e8c531ec9a2cd651da7b"
},
"gst-plugins-good": {
"version": "1.24.13",
"url": "https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-1.24.13.tar.xz",
"sha256": "574ac6f9fd84b32eb04e80572391d6762df6f9802a47bc0386cd6cc48c14d08b"
}
}
+152
View File
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Publish only UxPlay's records, independently of the system Avahi D-Bus policy.
#include "airplay_mdns.h"
#include <avahi-core/core.h>
#include <avahi-core/publish.h>
#include <avahi-core/log.h>
#include <avahi-common/alternative.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include <avahi-common/strlst.h>
#include <avahi-common/thread-watch.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
struct Service {
std::string name, type;
uint16_t port;
AvahiStringList* txt = nullptr;
AvahiSEntryGroup* group = nullptr;
~Service() {
if (group) avahi_s_entry_group_free(group);
avahi_string_list_free(txt);
}
};
class Publisher {
AvahiThreadedPoll* poll = nullptr;
AvahiServer* server = nullptr;
std::vector<std::unique_ptr<Service>> services;
static void group_state(AvahiServer* server, AvahiSEntryGroup*, AvahiEntryGroupState state, void* data) {
auto* service = static_cast<Service*>(data);
if (state == AVAHI_ENTRY_GROUP_ESTABLISHED)
std::fprintf(stderr, "AirPlay mDNS: published %s\n", service->type.c_str());
else if (state == AVAHI_ENTRY_GROUP_COLLISION || state == AVAHI_ENTRY_GROUP_FAILURE) {
std::fprintf(stderr, "AirPlay mDNS: cannot publish %s: %s\n", service->type.c_str(),
state == AVAHI_ENTRY_GROUP_COLLISION ? "service name conflict" : avahi_strerror(avahi_server_errno(server)));
std::exit(1);
}
}
void publish(Service& service) {
if (!service.group) service.group = avahi_s_entry_group_new(server, group_state, &service);
if (!service.group) throw std::runtime_error(avahi_strerror(avahi_server_errno(server)));
if (!avahi_s_entry_group_is_empty(service.group)) return;
int error = avahi_server_add_service_strlst(server, service.group, AVAHI_IF_UNSPEC,
AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0), service.name.c_str(), service.type.c_str(),
nullptr, nullptr, service.port, service.txt);
if (error >= 0) error = avahi_s_entry_group_commit(service.group);
if (error < 0) throw std::runtime_error(avahi_strerror(error));
}
static void server_state(AvahiServer* server, AvahiServerState state, void* data) {
auto* self = static_cast<Publisher*>(data);
// avahi_server_new can invoke us before it returns.
self->server = server;
try {
if (state == AVAHI_SERVER_RUNNING) {
for (auto& service : self->services) self->publish(*service);
} else if (state == AVAHI_SERVER_REGISTERING || state == AVAHI_SERVER_COLLISION) {
for (auto& service : self->services)
if (service->group) avahi_s_entry_group_reset(service->group);
if (state == AVAHI_SERVER_COLLISION) {
char* name = avahi_alternative_host_name(avahi_server_get_host_name(server));
int error = avahi_server_set_host_name(server, name);
avahi_free(name);
if (error < 0) throw std::runtime_error(avahi_strerror(error));
}
} else if (state == AVAHI_SERVER_FAILURE) {
throw std::runtime_error(avahi_strerror(avahi_server_errno(server)));
}
} catch (const std::exception& error) {
std::fprintf(stderr, "AirPlay mDNS: %s\n", error.what());
std::exit(1);
}
}
public:
Publisher() {
avahi_set_log_function([](AvahiLogLevel level, const char* message) {
if (level <= AVAHI_LOG_WARN) std::fprintf(stderr, "AirPlay mDNS: %s\n", message);
});
poll = avahi_threaded_poll_new();
if (!poll) throw std::runtime_error("cannot create mDNS poll");
AvahiServerConfig config;
avahi_server_config_init(&config);
// Share the machine hostname with other local mDNS stacks, avoiding
// conflicting reverse address records for the same network interfaces.
config.publish_hinfo = config.publish_workstation = config.publish_domain = 0;
config.enable_wide_area = 0;
int error = 0;
server = avahi_server_new(avahi_threaded_poll_get(poll), &config, server_state, this, &error);
avahi_server_config_free(&config);
if (!server || avahi_threaded_poll_start(poll) < 0) {
if (server) avahi_server_free(server);
avahi_threaded_poll_free(poll);
throw std::runtime_error(error ? avahi_strerror(error) : "cannot start mDNS poll");
}
}
~Publisher() {
avahi_threaded_poll_stop(poll);
services.clear();
avahi_server_free(server);
avahi_threaded_poll_free(poll);
}
Service* add(const char* name, const char* type, uint16_t port, const void* txt, uint16_t size) {
std::unique_ptr<Service> service(new Service);
service->name = name; service->type = type; service->port = ntohs(port);
if (avahi_string_list_parse(txt, size, &service->txt) < 0)
throw std::runtime_error("invalid mDNS TXT record");
avahi_threaded_poll_lock(poll);
try {
if (avahi_server_get_state(server) == AVAHI_SERVER_RUNNING) publish(*service);
auto* result = service.get();
services.push_back(std::move(service));
avahi_threaded_poll_unlock(poll);
return result;
} catch (...) {
// Free any registered group while its poll is still locked.
service.reset();
avahi_threaded_poll_unlock(poll);
throw;
}
}
void remove(Service* service) {
avahi_threaded_poll_lock(poll);
services.erase(std::remove_if(services.begin(), services.end(),
[service](const std::unique_ptr<Service>& item) { return item.get() == service; }), services.end());
avahi_threaded_poll_unlock(poll);
}
};
Publisher& publisher() { static Publisher instance; return instance; }
}
extern "C" DNSServiceErrorType framely_mdns_register(DNSServiceRef* result, DNSServiceFlags flags,
uint32_t interface_index, const char* name, const char* type, const char* domain, const char* host,
uint16_t port, uint16_t size, const void* txt, DNSServiceRegisterReply callback, void*) {
if (!result || !name || !type || flags || interface_index || domain || host || callback)
return kDNSServiceErr_BadParam;
try {
*result = reinterpret_cast<DNSServiceRef>(publisher().add(name, type, port, txt, size));
return kDNSServiceErr_NoError;
} catch (const std::exception& error) {
std::fprintf(stderr, "AirPlay mDNS initialization failed: %s\n", error.what());
return kDNSServiceErr_Unknown;
}
}
extern "C" void framely_mdns_deallocate(DNSServiceRef service) {
publisher().remove(reinterpret_cast<Service*>(service));
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <dns_sd.h>
#ifdef __cplusplus
extern "C" {
#endif
DNSServiceErrorType framely_mdns_register(DNSServiceRef*, DNSServiceFlags, uint32_t,
const char*, const char*, const char*, const char*, uint16_t, uint16_t,
const void*, DNSServiceRegisterReply, void*);
void framely_mdns_deallocate(DNSServiceRef);
#ifdef __cplusplus
}
#endif
+16 -12
View File
@@ -265,17 +265,21 @@ int main(int argc,char** argv) {
try {
if (argc<4) throw std::runtime_error("receiver DIRECTORY XMLDIR RECEIVE");
directory=argv[1]; xml_directory=argv[2]; receive_enabled=!strcmp(argv[3],"1"); gst_init(&argc,&argv);
// Frame's V4L2/Vulkan decoders can return corrupted frames and block
// playback controls. Prefer the bundled FFmpeg software decoders.
for (const char* name : {"avdec_h264", "avdec_h265"}) {
auto* decoder=gst_element_factory_find(name);
if (!decoder) throw std::runtime_error(std::string("Required decoder unavailable: ")+name);
gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(decoder),GST_RANK_PRIMARY+100);
gst_object_unref(decoder);
}
for (const char* name : {"v4l2h264dec", "v4l2h265dec", "vulkanh264dec", "vulkanh265dec"}) {
auto* decoder=gst_element_factory_find(name);
if (decoder) { gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(decoder),GST_RANK_NONE); gst_object_unref(decoder); }
// Decoder policy is only relevant to local DLNA playback. AirPlay uses
// UxPlay's own pipeline; discovery/control alone needs no video decoder.
if (receive_enabled) {
// Frame's V4L2/Vulkan decoders can return corrupted frames and block
// playback controls. Prefer the bundled FFmpeg software decoders.
for (const char* name : {"avdec_h264", "avdec_h265"}) {
auto* decoder=gst_element_factory_find(name);
if (!decoder) throw std::runtime_error(std::string("Required decoder unavailable: ")+name);
gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(decoder),GST_RANK_PRIMARY+100);
gst_object_unref(decoder);
}
for (const char* name : {"v4l2h264dec", "v4l2h265dec", "vulkanh264dec", "vulkanh265dec"}) {
auto* decoder=gst_element_factory_find(name);
if (decoder) { gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(decoder),GST_RANK_NONE); gst_object_unref(decoder); }
}
}
auto* manager=gupnp_context_manager_create_full(GSSDP_UDA_VERSION_1_0,G_SOCKET_FAMILY_IPV4,0);
g_signal_connect(manager,"context-available",G_CALLBACK(context_available),nullptr);
@@ -285,5 +289,5 @@ int main(int argc,char** argv) {
for (auto* p:transports) g_object_unref(p);
for (auto* p:renderers) g_object_unref(p);
g_io_channel_unref(channel); g_object_unref(manager); g_main_loop_unref(loop); return 0;
} catch (const std::exception& e) { output({{"event","fatal"},{"error",e.what()}}); return 1; }
} catch (const std::exception& e) { std::cerr << "DLNA receiver: " << e.what() << std::endl; output({{"event","fatal"},{"error",e.what()}}); return 1; }
}
+41 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Add only lifecycle events to UxPlay; protocol/decoder code remains upstream."""
"""Adapt UxPlay lifecycle events and explicit sinks; protocol/decoder code remains upstream."""
from pathlib import Path
import sys
p=Path(sys.argv[1])/'uxplay.cpp'
@@ -123,3 +123,43 @@ if 'Framely HLS consent' not in s:
}
''',1)
renderer.write_text(s)
# Framely supplies both sinks explicitly; autodetect is not part of its pipeline.
# Keep upstream checks unchanged for all other UxPlay output modes.
audio=p.parent/'renderers/audio_renderer.c'
s=audio.read_text()
if 'framely_explicit_sinks' not in s:
assert 'static gboolean check_plugins (void)' in s
s=s.replace('static gboolean check_plugins (void)', 'static gboolean check_plugins (bool needs_autodetect)',1)
marker=' GstPlugin *plugin = NULL;'
assert marker in s
s=s.replace(marker, ' /* framely_explicit_sinks: no automatic device selection needed. */\n if (!needs_autodetect && !strcmp(needed[i], "autodetect")) continue;\n'+marker,1)
s=s.replace('bool gstreamer_init(){','bool gstreamer_init(bool needs_autodetect){',1)
s=s.replace('return (bool) check_plugins ();','return (bool) check_plugins (needs_autodetect);',1)
audio.write_text(s)
header=p.parent/'renderers/audio_renderer.h'
s=header.read_text()
if 'bool gstreamer_init();' in s:
header.write_text(s.replace('bool gstreamer_init();','bool gstreamer_init(bool needs_autodetect);',1))
s=p.read_text()
if 'if (!gstreamer_init())' in s:
s=s.replace('if (!gstreamer_init())', 'if (!gstreamer_init(!(videosink == "framelyvideosink" && audiosink == "framelyaudiosink")))',1)
p.write_text(s)
# SteamOS can disable publication in the system Avahi daemon. Use its existing
# Avahi core library inside the receiver instead; leave system policy untouched.
for name in ('airplay_mdns.h', 'airplay_mdns.cpp'):
copyfile(Path(__file__).parent/'native'/name, p.parent/'framely'/name)
dns=p.parent/'lib/dnssd.c'
s=dns.read_text()
if 'framely_mdns_register' not in s:
s=s.replace('#include <dns_sd.h>', '#include <dns_sd.h>\n#include "../framely/airplay_mdns.h"', 1)
marker=' dnssd->name_len = name_len;'
assert marker in s
s=s.replace(marker, ' if (getenv("FRAMELY_CAST_DIR")) {\n dnssd->DNSServiceRegister = &framely_mdns_register;\n dnssd->DNSServiceRefDeallocate = &framely_mdns_deallocate;\n }\n\n'+marker, 1)
dns.write_text(s)
cmake=p.parent/'lib/CMakeLists.txt'
s=cmake.read_text()
if 'framely/airplay_mdns.cpp' not in s:
s+='\ntarget_sources(airplay PRIVATE ../framely/airplay_mdns.cpp)\npkg_check_modules(FRAMELY_AVAHI REQUIRED IMPORTED_TARGET avahi-core)\ntarget_link_libraries(airplay PUBLIC PkgConfig::FRAMELY_AVAHI)\n'
cmake.write_text(s)
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
base=$(cd -- "$(dirname -- "$0")/.." && pwd)
cd "$base"
# SteamOS does not provide these Good plugins. Build only the elements used
# by our receive pipelines; keep GStreamer, PulseAudio and Soup system libraries.
meson setup media/source/gst-good-build media/source/gst-plugins-good-1.24.13 \
--prefix=/usr --libdir=lib --buildtype=release --wrap-mode=nofallback \
-Dauto_features=disabled -Dlevel=enabled -Dpulse=enabled \
-Dvideofilter=enabled -Disomp4=enabled -Dsoup=enabled \
-Dtests=disabled -Dexamples=disabled -Ddoc=disabled
ninja -C media/source/gst-good-build -j"${FRAMELY_BUILD_JOBS:-4}"
DESTDIR="$base/media/source/gst-good-install" meson install -C media/source/gst-good-build --no-rebuild
for plugin in level pulseaudio videofilter isomp4 soup; do
cp "media/source/gst-good-install/usr/lib/gstreamer-1.0/libgst${plugin}.so" media/bin/
done
cp media/source/gst-plugins-good-1.24.13/COPYING media/bin/LICENSE.gst-good
+1
View File
@@ -23,6 +23,7 @@ cp "$capture_target/release/framely-capture" "$capture_target/release/framely-pa
# Native dependencies use the target system's GStreamer and GUPnP libraries.
python3 tools/fetch-media.py
bash tools/build-gst-libav.sh
bash tools/build-gst-good.sh
python3 media/patch-uxplay.py media/source/UxPlay-1.73.7
cmake -S media/source/UxPlay-1.73.7 -B media/source/uxplay-build -DNO_X11_DEPS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build media/source/uxplay-build -j"${FRAMELY_BUILD_JOBS:-4}"
+5 -2
View File
@@ -16,7 +16,7 @@ if [[ ${FRAMELY_USE_PREBUILT_MEDIA:-0} == 1 ]]; then
else
[[ -z $(git status --porcelain -- media) ]] || { echo 'Media sources changed; rebuild media.' >&2; exit 1; }
fi
for file in media/bin/framely-panel-grab media/bin/framely-capture media/bin/mediamtx media/bin/uxplay media/bin/framely-receiver media/bin/libgstframely.so media/bin/libgstlibav.so media/bin/LICENSE media/bin/LICENSE.uxplay media/bin/LICENSE.gst-libav media/bin/LICENSE.llhttp; do
for file in media/bin/framely-panel-grab media/bin/framely-capture media/bin/mediamtx media/bin/uxplay media/bin/framely-receiver media/bin/libgstframely.so media/bin/libgstlibav.so media/bin/LICENSE media/bin/LICENSE.uxplay media/bin/LICENSE.gst-libav media/bin/LICENSE.llhttp media/bin/LICENSE.gst-good media/bin/libgstlevel.so media/bin/libgstpulseaudio.so media/bin/libgstvideofilter.so media/bin/libgstisomp4.so media/bin/libgstsoup.so; do
[[ -s $file ]] || { echo "Missing prebuilt media: $file" >&2; exit 1; }
done
else
@@ -39,18 +39,21 @@ mkdir -p "$stage"/{bin,lib/cef,lib/openvr,lib/media,share/ui,share/licenses,tool
cp "$binary" "$stage/bin/"
cp media/bin/framely-panel-grab "$stage/bin/"
cp media/bin/framely-capture media/bin/mediamtx media/bin/uxplay media/bin/framely-receiver media/bin/libgstframely.so media/bin/libgstlibav.so "$stage/lib/media/"
cp media/bin/libgst{level,pulseaudio,videofilter,isomp4,soup}.so "$stage/lib/media/"
cp -a media/bin/upnp "$stage/lib/media/"
cp media/bin/LICENSE "$stage/share/licenses/mediamtx.txt"
cp media/bin/LICENSE.uxplay "$stage/share/licenses/uxplay.txt"
cp media/bin/LICENSE.gst-libav "$stage/share/licenses/gst-libav.txt"
cp media/bin/LICENSE.gst-good "$stage/share/licenses/gst-plugins-good.txt"
cp media/bin/LICENSE.llhttp "$stage/share/licenses/uxplay-llhttp.txt"
cp media/capture/LICENSE.framecorder "$stage/share/licenses/panel-capture.txt"
cp media/THIRD_PARTY_NOTICES.md "$stage/share/licenses/media-notices.md"
mkdir -p "$stage/share/source/media/capture" "$stage/share/source/tools" "$stage/share/source/native/vendor"
cp -a media/native media/upnp media/patch-uxplay.py media/dependencies.json "$stage/share/source/media/"
cp -a media/capture/src media/capture/shaders media/capture/Cargo.toml media/capture/Cargo.lock media/capture/build.rs media/capture/LICENSE.framecorder "$stage/share/source/media/capture/"
cp tools/build-media.sh tools/build-gst-libav.sh tools/fetch-media.py "$stage/share/source/tools/"
cp tools/build-media.sh tools/build-gst-libav.sh tools/build-gst-good.sh tools/fetch-media.py "$stage/share/source/tools/"
cp native/vendor/json.hpp "$stage/share/source/native/vendor/"
tar -cJf "$stage/share/source/media/gst-plugins-good.tar.xz" -C media/source gst-plugins-good-1.24.13
tar -cJf "$stage/share/source/media/gst-libav.tar.xz" -C media/source gst-libav-1.24.13
tar -czf "$stage/share/source/media/uxplay.tar.gz" -C media/source UxPlay-1.73.7
+32 -1
View File
@@ -9,10 +9,41 @@ binaries=Path(args.binaries).resolve()
with tempfile.TemporaryDirectory(prefix='framely-media-test-') as directory:
root=Path(directory);events=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM);events.bind(str(root/'events.sock'));events.settimeout(5)
token='0123456789abcdef0123456789abcdef';env=dict(os.environ,GST_PLUGIN_PATH=str(binaries),FRAMELY_CAST_DIR=directory,FRAMELY_CAST_ID=token)
airplay=subprocess.Popen([str(binaries/'uxplay'),'-n','Framely test','-avdec','-hls','-vs','framelyvideosink','-as','framelyaudiosink'],env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
# Check the actual receive elements, including plugins absent on stock Frame.
for element in ['framelyvideosink','framelyaudiosink','level','pulsesink','videoflip','souphttpsrc','qtdemux']:
result=subprocess.run(['gst-inspect-1.0','--exists',element],env=env,capture_output=True)
assert result.returncode==0,f'Required receive element unavailable: {element}'
test_name=f'Framely test {os.getpid()}'
airplay=subprocess.Popen([str(binaries/'uxplay'),'-n',test_name,'-nh','-m','-avdec','-hls','-vs','framelyvideosink','-as','framelyaudiosink'],env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
try:
time.sleep(2);assert airplay.poll() is None,airplay.stdout.read().decode()
print('PASS: UxPlay starts with bundled decoders and consent sinks')
# Use the system daemon only to browse. Publication must also work when
# that daemon's disable-publishing policy is enabled (Frame's default).
import shutil
if shutil.which('avahi-browse'):
for service_type in ['_airplay._tcp','_raop._tcp']:
browser=subprocess.Popen(['avahi-browse','--resolve','--parsable',
'--no-db-lookup',service_type],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,text=True)
from queue import Queue,Empty
discovered=Queue()
def browse_lines():
for line in browser.stdout:discovered.put(line)
Thread(target=browse_lines,daemon=True).start()
try:
deadline=time.monotonic()+15
while True:
try:line=discovered.get(timeout=max(.01,deadline-time.monotonic()))
except Empty:raise AssertionError(f'No resolved {service_type} advertisement')
if line.startswith('=;') and test_name in line.replace('\\032',' '):
assert 'features=' in line or 'cn=' in line,line
break
assert time.monotonic()<deadline,f'No resolved {service_type} advertisement'
finally:
browser.terminate();browser.wait(timeout=3)
print('PASS: AirPlay and RAOP multicast discovery, address resolution and TXT records')
else:
print('SKIP: multicast discovery test requires avahi-browse')
finally:
airplay.terminate();airplay.wait(timeout=3)
p=subprocess.Popen(['gst-launch-1.0','-q','videotestsrc','is-live=true','!','video/x-raw,width=320,height=240,framerate=10/1','!','framelyvideosink'],env=env,stderr=subprocess.PIPE)