mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-25 21:01:49 +01:00
Fix AirPlay startup and service discovery on Frame
This commit is contained in:
1 parent
f7ae2e8e2c
commit
fc381c2676
12 files changed
+292
-21
No files matched your search
@@ -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
@@ -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`。
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
Executable
+17
@@ -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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user