Fix YouTube relay instabile e timer LIVE a zero in app

ffmpeg: probesize/analyzeduration su _air, restart più rapido e avvio relay
al go_live. App: sincronizza started_at dal server e fallback al passaggio live.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 08:56:59 +02:00
parent 841e99c775
commit fde1213ca5
4 changed files with 30 additions and 3 deletions

View File

@@ -17,7 +17,9 @@ module Mediamtx
# RTMP ancora connesso in pausa: non forzare live/reconnect. # RTMP ancora connesso in pausa: non forzare live/reconnect.
elsif @session.may_go_live? elsif @session.may_go_live?
@session.go_live! @session.go_live!
Streams::Overlay::Refresh.call(@session.reload) @session.reload
Streams::Overlay::Refresh.call(@session)
Streams::YoutubeRelay.ensure_publishing!(@session) if @session.platform == "youtube"
elsif @session.reconnecting? && @session.may_reconnect? elsif @session.reconnecting? && @session.may_reconnect?
@session.reconnect! @session.reconnect!
cancel_timeout cancel_timeout

View File

@@ -56,10 +56,13 @@ module Streams
return if session.stream_key.blank? return if session.stream_key.blank?
return unless Streams::OverlayRelay.running?(session.id) return unless Streams::OverlayRelay.running?(session.id)
pid = pid_for(session.id)
clear_pid(session.id) if pid.present? && !process_alive?(pid.to_i)
return if running?(session.id) return if running?(session.id)
last_restart = redis.get(restart_debounce_key(session.id)).to_i last_restart = redis.get(restart_debounce_key(session.id)).to_i
return if last_restart.positive? && (Time.now.to_i - last_restart) < 20 return if last_restart.positive? && (Time.now.to_i - last_restart) < 5
start(session) start(session)
redis.set(restart_debounce_key(session.id), Time.now.to_i, ex: 300) redis.set(restart_debounce_key(session.id), Time.now.to_i, ex: 300)
@@ -75,6 +78,7 @@ module Streams
[ [
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "warning", "ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "warning",
"-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1", "-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1",
"-analyzeduration", "10000000", "-probesize", "10000000",
"-rw_timeout", "15000000", "-rw_timeout", "15000000",
"-i", intake, "-i", intake,
"-f", "lavfi", "-i", "anullsrc=channel_layout=mono:sample_rate=48000", "-f", "lavfi", "-i", "anullsrc=channel_layout=mono:sample_rate=48000",

View File

@@ -41,6 +41,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> {
static const _previewKey = ValueKey<String>('camera_streaming_preview'); static const _previewKey = ValueKey<String>('camera_streaming_preview');
Timer? _elapsedTimer; Timer? _elapsedTimer;
Timer? _sessionSyncTimer;
Timer? _telemetryTimer; Timer? _telemetryTimer;
Timer? _statsTimer; Timer? _statsTimer;
Timer? _scorePullTimer; Timer? _scorePullTimer;
@@ -75,6 +76,9 @@ class _CameraScreenState extends ConsumerState<CameraScreen> {
); );
} }
}); });
_sessionSyncTimer = Timer.periodic(const Duration(seconds: 15), (_) {
unawaited(_syncSessionStartedAt());
});
_telemetryTimer = Timer.periodic(const Duration(seconds: 10), (_) => _sendTelemetry()); _telemetryTimer = Timer.periodic(const Duration(seconds: 10), (_) => _sendTelemetry());
_statsTimer = Timer.periodic(const Duration(seconds: 30), (_) => _fetchStats()); _statsTimer = Timer.periodic(const Duration(seconds: 30), (_) => _fetchStats());
_scorePullTimer = Timer.periodic(const Duration(seconds: 2), (_) { _scorePullTimer = Timer.periodic(const Duration(seconds: 2), (_) {
@@ -263,6 +267,20 @@ class _CameraScreenState extends ConsumerState<CameraScreen> {
} catch (_) {} } catch (_) {}
} }
/// Il server imposta `started_at` solo al primo go_live; sincronizza per il timer LIVE.
Future<void> _syncSessionStartedAt() async {
final current = ref.read(sessionProvider).session;
if (current == null || current.startedAt != null || !current.isLive) return;
try {
final fresh = await ref.read(apiClientProvider).fetchSession(widget.sessionId);
if (!mounted || fresh.startedAt == null) return;
ref.read(sessionProvider.notifier).setSession(
fresh,
match: ref.read(sessionProvider).match,
);
} catch (_) {}
}
Future<void> _lockOrientations() async { Future<void> _lockOrientations() async {
await SystemChrome.setPreferredOrientations([ await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeLeft,
@@ -497,6 +515,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> {
@override @override
void dispose() { void dispose() {
_elapsedTimer?.cancel(); _elapsedTimer?.cancel();
_sessionSyncTimer?.cancel();
_telemetryTimer?.cancel(); _telemetryTimer?.cancel();
_statsTimer?.cancel(); _statsTimer?.cancel();
_scorePullTimer?.cancel(); _scorePullTimer?.cancel();

View File

@@ -111,6 +111,8 @@ class SessionNotifier extends StateNotifier<SessionState> {
state = state.copyWith(cameraDevice: device); state = state.copyWith(cameraDevice: device);
return; return;
} }
final startedAt = s.startedAt ??
((incoming == 'live' || incoming == 'reconnecting') ? DateTime.now() : null);
state = state.copyWith( state = state.copyWith(
session: StreamSession( session: StreamSession(
id: s.id, id: s.id,
@@ -127,7 +129,7 @@ class SessionNotifier extends StateNotifier<SessionState> {
privacyStatus: s.privacyStatus, privacyStatus: s.privacyStatus,
qualityPreset: s.qualityPreset, qualityPreset: s.qualityPreset,
targetBitrate: s.targetBitrate, targetBitrate: s.targetBitrate,
startedAt: s.startedAt, startedAt: startedAt,
disconnectionCount: s.disconnectionCount, disconnectionCount: s.disconnectionCount,
score: s.score, score: s.score,
devices: s.devices, devices: s.devices,