YouTube come destinazione pubblica, non Match Live TV.
Non propone più il link MLTV per sessioni YouTube; la pagina /live reindirizza a YouTube; il relay verso YouTube si riavvia al connect. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -146,10 +146,11 @@ module Api
|
|||||||
platform: session.platform,
|
platform: session.platform,
|
||||||
rtmp_ingest_url: session.rtmp_ingest_url,
|
rtmp_ingest_url: session.rtmp_ingest_url,
|
||||||
hls_playback_url: session.hls_playback_url,
|
hls_playback_url: session.hls_playback_url,
|
||||||
watch_page_url: session.watch_page_url,
|
watch_page_url: session.matchlivetv_platform? ? session.watch_page_url : nil,
|
||||||
share_url: session.share_url,
|
share_url: session.share_url,
|
||||||
youtube_watch_url: session.youtube_watch_url,
|
youtube_watch_url: session.youtube_watch_url,
|
||||||
youtube_broadcast_id: session.youtube_broadcast_id,
|
youtube_broadcast_id: session.youtube_broadcast_id,
|
||||||
|
youtube_ready: session.youtube_ready?,
|
||||||
privacy_status: session.privacy_status,
|
privacy_status: session.privacy_status,
|
||||||
quality_preset: session.quality_preset,
|
quality_preset: session.quality_preset,
|
||||||
target_bitrate: session.target_bitrate,
|
target_bitrate: session.target_bitrate,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module Public
|
|||||||
sessions = StreamSession
|
sessions = StreamSession
|
||||||
.broadcasting
|
.broadcasting
|
||||||
.publicly_listed
|
.publicly_listed
|
||||||
|
.where(platform: "matchlivetv")
|
||||||
.includes(:score_state, match: { team: :club })
|
.includes(:score_state, match: { team: :club })
|
||||||
sessions = sessions.joins(:match).where(matches: { team_id: team_ids }) if team_ids.present?
|
sessions = sessions.joins(:match).where(matches: { team_id: team_ids }) if team_ids.present?
|
||||||
|
|
||||||
|
|||||||
@@ -119,11 +119,13 @@ class StreamSession < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def share_url
|
def share_url
|
||||||
if platform == "youtube"
|
return youtube_watch_url if platform == "youtube"
|
||||||
youtube_watch_url.presence || watch_page_url
|
|
||||||
else
|
watch_page_url
|
||||||
watch_page_url
|
end
|
||||||
end
|
|
||||||
|
def youtube_ready?
|
||||||
|
platform == "youtube" && youtube_watch_url.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_only?
|
def link_only?
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ module Streams
|
|||||||
pid.present? && process_alive?(pid)
|
pid.present? && process_alive?(pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Riavvia il relay se è morto (es. avviato prima del segnale su _air).
|
||||||
|
def ensure_publishing!(session)
|
||||||
|
return unless session.platform == "youtube"
|
||||||
|
return if session.terminal?
|
||||||
|
return if session.stream_key.blank?
|
||||||
|
|
||||||
|
return if running?(session.id)
|
||||||
|
|
||||||
|
start(session)
|
||||||
|
rescue Error => e
|
||||||
|
Rails.logger.warn("[YoutubeRelay] ensure_publishing session=#{session.id}: #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def mediamtx_intake_url(session)
|
def mediamtx_intake_url(session)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module Webhooks
|
|||||||
return if session.paused?
|
return if session.paused?
|
||||||
|
|
||||||
Streams::OverlayRelay.ensure_publishing!(session)
|
Streams::OverlayRelay.ensure_publishing!(session)
|
||||||
|
Streams::YoutubeRelay.ensure_publishing!(session) if session.platform == "youtube"
|
||||||
|
|
||||||
return if session.live? && session.stream_events.where(event_type: "connected").exists?
|
return if session.live? && session.stream_events.where(event_type: "connected").exists?
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ module Youtube
|
|||||||
metadata: { broadcast_id: broadcast[:broadcast_id] },
|
metadata: { broadcast_id: broadcast[:broadcast_id] },
|
||||||
occurred_at: Time.current
|
occurred_at: Time.current
|
||||||
)
|
)
|
||||||
|
SessionChannel.broadcast_message(
|
||||||
|
@session,
|
||||||
|
{
|
||||||
|
type: "youtube_ready",
|
||||||
|
youtube_watch_url: @session.youtube_watch_url,
|
||||||
|
share_url: @session.share_url,
|
||||||
|
youtube_broadcast_id: @session.youtube_broadcast_id
|
||||||
|
}
|
||||||
|
)
|
||||||
@session
|
@session
|
||||||
rescue BroadcastService::Error => e
|
rescue BroadcastService::Error => e
|
||||||
@session.stream_events.create!(
|
@session.stream_events.create!(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<% content_for :robots, "noindex, follow" %>
|
<% content_for :robots, "noindex, follow" %>
|
||||||
|
|
||||||
<% content_for :head do %>
|
<% content_for :head do %>
|
||||||
<% unless @stream_closed %>
|
<% unless @stream_closed || @session.platform == "youtube" %>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -25,6 +25,19 @@
|
|||||||
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
|
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
|
||||||
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
|
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
|
||||||
</div>
|
</div>
|
||||||
|
<% elsif @session.platform == "youtube" %>
|
||||||
|
<div class="stream-ended youtube-live-cta" role="status" aria-live="polite">
|
||||||
|
<div class="stream-ended-icon" aria-hidden="true">▶</div>
|
||||||
|
<h2>Diretta su YouTube</h2>
|
||||||
|
<% if @session.youtube_watch_url.present? %>
|
||||||
|
<p>Questa partita è trasmessa sul canale YouTube Match Live TV, non sul player del sito.</p>
|
||||||
|
<%= link_to "Guarda su YouTube", @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %>
|
||||||
|
<% else %>
|
||||||
|
<p>Stiamo preparando la diretta sul canale YouTube. Aggiorna tra qualche secondo.</p>
|
||||||
|
<meta http-equiv="refresh" content="8">
|
||||||
|
<% end %>
|
||||||
|
<p class="stream-ended-hint">Condividi solo il link YouTube con tifosi e società.</p>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="live-player-wrap">
|
<div class="live-player-wrap">
|
||||||
<video id="player" controls playsinline muted autoplay></video>
|
<video id="player" controls playsinline muted autoplay></video>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
int _latencyMs = 0;
|
int _latencyMs = 0;
|
||||||
String _networkType = '—';
|
String _networkType = '—';
|
||||||
bool _starting = false;
|
bool _starting = false;
|
||||||
|
Timer? _youtubePoll;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -54,6 +55,45 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
if (session?.score != null) {
|
if (session?.score != null) {
|
||||||
ref.read(scoreProvider.notifier).reset(session!.score!);
|
ref.read(scoreProvider.notifier).reset(session!.score!);
|
||||||
}
|
}
|
||||||
|
_startYoutubeLinkPoll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_youtubePoll?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startYoutubeLinkPoll() {
|
||||||
|
final session = ref.read(sessionProvider).session;
|
||||||
|
if (session == null || session.platform != 'youtube') return;
|
||||||
|
if (session.youtubeReady || (session.youtubeWatchUrl?.isNotEmpty ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_youtubePoll?.cancel();
|
||||||
|
var attempts = 0;
|
||||||
|
_youtubePoll = Timer.periodic(const Duration(seconds: 2), (_) async {
|
||||||
|
attempts++;
|
||||||
|
if (!mounted || attempts > 30) {
|
||||||
|
_youtubePoll?.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final current = ref.read(sessionProvider).session;
|
||||||
|
if (current == null) return;
|
||||||
|
try {
|
||||||
|
final updated =
|
||||||
|
await ref.read(apiClientProvider).fetchSession(current.id);
|
||||||
|
ref
|
||||||
|
.read(sessionProvider.notifier)
|
||||||
|
.setSession(updated, match: ref.read(sessionProvider).match);
|
||||||
|
if (updated.youtubeReady ||
|
||||||
|
(updated.youtubeWatchUrl?.isNotEmpty ?? false)) {
|
||||||
|
_youtubePoll?.cancel();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +280,24 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
|
if (_testCompleted &&
|
||||||
|
session != null &&
|
||||||
|
session.platform == 'youtube' &&
|
||||||
|
shareUrl == null) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surfaceElevated,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Preparazione diretta sul canale YouTube Match Live TV… '
|
||||||
|
'Il link apparirà qui tra pochi secondi.',
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
if (_testCompleted && session != null && shareUrl != null) ...[
|
if (_testCompleted && session != null && shareUrl != null) ...[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
@@ -252,7 +310,7 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
session.platform == 'youtube'
|
session.platform == 'youtube'
|
||||||
? 'Link video YouTube'
|
? 'Link diretta YouTube (canale Match Live TV)'
|
||||||
: 'Link diretta Match Live TV',
|
: 'Link diretta Match Live TV',
|
||||||
style: theme.textTheme.titleSmall,
|
style: theme.textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
@@ -261,6 +319,13 @@ class _StepNetworkTestState extends ConsumerState<StepNetworkTest> {
|
|||||||
shareUrl,
|
shareUrl,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
|
if (session.platform == 'youtube') ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Condividi solo questo link: gli spettatori guardano su YouTube, non sul sito.',
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -47,6 +47,38 @@ class SessionNotifier extends StateNotifier<SessionState> {
|
|||||||
state = state.copyWith(session: session, match: match);
|
state = state.copyWith(session: session, match: match);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateYoutubeLinks({
|
||||||
|
required String? youtubeWatchUrl,
|
||||||
|
required String? shareUrl,
|
||||||
|
String? youtubeBroadcastId,
|
||||||
|
bool youtubeReady = true,
|
||||||
|
}) {
|
||||||
|
final s = state.session;
|
||||||
|
if (s == null || s.platform != 'youtube') return;
|
||||||
|
state = state.copyWith(
|
||||||
|
session: StreamSession(
|
||||||
|
id: s.id,
|
||||||
|
matchId: s.matchId,
|
||||||
|
status: s.status,
|
||||||
|
platform: s.platform,
|
||||||
|
rtmpIngestUrl: s.rtmpIngestUrl,
|
||||||
|
hlsPlaybackUrl: s.hlsPlaybackUrl,
|
||||||
|
watchPageUrl: s.watchPageUrl,
|
||||||
|
shareUrl: shareUrl,
|
||||||
|
youtubeWatchUrl: youtubeWatchUrl,
|
||||||
|
youtubeBroadcastId: youtubeBroadcastId ?? s.youtubeBroadcastId,
|
||||||
|
youtubeReady: youtubeReady,
|
||||||
|
privacyStatus: s.privacyStatus,
|
||||||
|
qualityPreset: s.qualityPreset,
|
||||||
|
targetBitrate: s.targetBitrate,
|
||||||
|
startedAt: s.startedAt,
|
||||||
|
disconnectionCount: s.disconnectionCount,
|
||||||
|
score: s.score,
|
||||||
|
devices: s.devices,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void setMatch(MatchModel match) {
|
void setMatch(MatchModel match) {
|
||||||
state = state.copyWith(match: match);
|
state = state.copyWith(match: match);
|
||||||
}
|
}
|
||||||
@@ -91,6 +123,7 @@ class SessionNotifier extends StateNotifier<SessionState> {
|
|||||||
shareUrl: s.shareUrl,
|
shareUrl: s.shareUrl,
|
||||||
youtubeWatchUrl: s.youtubeWatchUrl,
|
youtubeWatchUrl: s.youtubeWatchUrl,
|
||||||
youtubeBroadcastId: s.youtubeBroadcastId,
|
youtubeBroadcastId: s.youtubeBroadcastId,
|
||||||
|
youtubeReady: s.youtubeReady,
|
||||||
privacyStatus: s.privacyStatus,
|
privacyStatus: s.privacyStatus,
|
||||||
qualityPreset: s.qualityPreset,
|
qualityPreset: s.qualityPreset,
|
||||||
targetBitrate: s.targetBitrate,
|
targetBitrate: s.targetBitrate,
|
||||||
@@ -133,7 +166,10 @@ class SessionNotifier extends StateNotifier<SessionState> {
|
|||||||
rtmpIngestUrl: session.rtmpIngestUrl,
|
rtmpIngestUrl: session.rtmpIngestUrl,
|
||||||
hlsPlaybackUrl: session.hlsPlaybackUrl,
|
hlsPlaybackUrl: session.hlsPlaybackUrl,
|
||||||
watchPageUrl: session.watchPageUrl,
|
watchPageUrl: session.watchPageUrl,
|
||||||
|
shareUrl: session.shareUrl,
|
||||||
|
youtubeWatchUrl: session.youtubeWatchUrl,
|
||||||
youtubeBroadcastId: session.youtubeBroadcastId,
|
youtubeBroadcastId: session.youtubeBroadcastId,
|
||||||
|
youtubeReady: session.youtubeReady,
|
||||||
privacyStatus: session.privacyStatus,
|
privacyStatus: session.privacyStatus,
|
||||||
qualityPreset: session.qualityPreset,
|
qualityPreset: session.qualityPreset,
|
||||||
targetBitrate: session.targetBitrate,
|
targetBitrate: session.targetBitrate,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class StreamSession {
|
|||||||
this.shareUrl,
|
this.shareUrl,
|
||||||
this.youtubeWatchUrl,
|
this.youtubeWatchUrl,
|
||||||
this.youtubeBroadcastId,
|
this.youtubeBroadcastId,
|
||||||
|
this.youtubeReady = false,
|
||||||
this.privacyStatus = 'public',
|
this.privacyStatus = 'public',
|
||||||
this.qualityPreset = '720p_30_2.5mbps',
|
this.qualityPreset = '720p_30_2.5mbps',
|
||||||
this.targetBitrate = 2500000,
|
this.targetBitrate = 2500000,
|
||||||
@@ -32,6 +33,7 @@ class StreamSession {
|
|||||||
final String? shareUrl;
|
final String? shareUrl;
|
||||||
final String? youtubeWatchUrl;
|
final String? youtubeWatchUrl;
|
||||||
final String? youtubeBroadcastId;
|
final String? youtubeBroadcastId;
|
||||||
|
final bool youtubeReady;
|
||||||
final String privacyStatus;
|
final String privacyStatus;
|
||||||
final String qualityPreset;
|
final String qualityPreset;
|
||||||
final int targetBitrate;
|
final int targetBitrate;
|
||||||
@@ -62,6 +64,7 @@ class StreamSession {
|
|||||||
shareUrl: json['share_url'] as String?,
|
shareUrl: json['share_url'] as String?,
|
||||||
youtubeWatchUrl: json['youtube_watch_url'] as String?,
|
youtubeWatchUrl: json['youtube_watch_url'] as String?,
|
||||||
youtubeBroadcastId: json['youtube_broadcast_id'] as String?,
|
youtubeBroadcastId: json['youtube_broadcast_id'] as String?,
|
||||||
|
youtubeReady: json['youtube_ready'] as bool? ?? false,
|
||||||
privacyStatus: json['privacy_status'] as String? ?? 'public',
|
privacyStatus: json['privacy_status'] as String? ?? 'public',
|
||||||
qualityPreset: json['quality_preset'] as String? ?? '720p_30_2.5mbps',
|
qualityPreset: json['quality_preset'] as String? ?? '720p_30_2.5mbps',
|
||||||
targetBitrate: json['target_bitrate'] as int? ?? 2500000,
|
targetBitrate: json['target_bitrate'] as int? ?? 2500000,
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'models/stream_session.dart';
|
import 'models/stream_session.dart';
|
||||||
|
|
||||||
String? watchShareUrl(StreamSession session) {
|
String? watchShareUrl(StreamSession session) {
|
||||||
return session.shareUrl ?? session.youtubeWatchUrl ?? session.watchPageUrl;
|
if (session.platform == 'youtube') {
|
||||||
|
return session.youtubeWatchUrl ?? session.shareUrl;
|
||||||
|
}
|
||||||
|
return session.shareUrl ?? session.watchPageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
String watchShareLabel(StreamSession session) {
|
String watchShareLabel(StreamSession session) {
|
||||||
|
|||||||
@@ -121,6 +121,13 @@ class WebsocketService {
|
|||||||
_ref.read(sessionProvider.notifier).applyCommand('resume_stream');
|
_ref.read(sessionProvider.notifier).applyCommand('resume_stream');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'youtube_ready':
|
||||||
|
_ref.read(sessionProvider.notifier).updateYoutubeLinks(
|
||||||
|
youtubeWatchUrl: data['youtube_watch_url'] as String?,
|
||||||
|
shareUrl: data['share_url'] as String?,
|
||||||
|
youtubeBroadcastId: data['youtube_broadcast_id'] as String?,
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user