resolvePreferredStream method
Implementation
Future<StreamChoice?> resolvePreferredStream(Scene scene) async {
final client = ref.read(graphqlClientProvider);
final mediaHeaders = ref.read(mediaHeadersProvider);
final queryStopwatch = Stopwatch()..start();
final result = await client.query(
QueryOptions(
document: gql('''
query SceneStreamsForPlayer(\$id: ID!) {
sceneStreams(id: \$id) {
url
mime_type
label
}
findScene(id: \$id) {
sceneStreams {
url
mime_type
label
}
}
}
'''),
variables: <String, dynamic>{'id': scene.id},
fetchPolicy: FetchPolicy.networkOnly,
cacheRereadPolicy: CacheRereadPolicy.ignoreAll,
),
);
queryStopwatch.stop();
final exceptionSummary = _summarizeException(result.exception);
AppLogStore.instance.add(
'resolver query scene=${scene.id} elapsed=${queryStopwatch.elapsedMilliseconds}ms hasException=${result.hasException}${exceptionSummary == null || exceptionSummary.isEmpty ? '' : ' error=$exceptionSummary'}',
source: 'stream_resolver',
);
final rootStreams =
((result.data?['sceneStreams']) as List?)
?.whereType<Map<String, dynamic>>()
.toList() ??
const <Map<String, dynamic>>[];
final nestedStreams =
((result.data?['findScene']?['sceneStreams']) as List?)
?.whereType<Map<String, dynamic>>()
.toList() ??
const <Map<String, dynamic>>[];
final streams = rootStreams.isNotEmpty ? rootStreams : nestedStreams;
if (result.hasException && streams.isEmpty) return null;
if (result.hasException && streams.isNotEmpty) {
AppLogStore.instance.add(
'resolver query scene=${scene.id} continuing with ${streams.length} streams despite exception',
source: 'stream_resolver',
);
}
final graphqlEndpoint = client.link is HttpLink
? (client.link as HttpLink).uri
: Uri.parse(scene.paths.stream ?? 'https://localhost/graphql');
if (streams.isEmpty) {
final streamUrl = scene.paths.stream ?? '';
if (streamUrl.isEmpty) return null;
return StreamChoice(url: streamUrl, mimeType: guessMimeType(streamUrl));
}
final candidates = <StreamChoice>[];
for (final stream in streams) {
final resolvedUrl = resolveGraphqlMediaUrl(
rawUrl: stream['url'] as String?,
graphqlEndpoint: graphqlEndpoint,
);
if (resolvedUrl.isEmpty) continue;
final mime = (stream['mime_type'] as String?)?.trim();
final label = (stream['label'] as String?)?.trim();
final guessed = guessMimeType(resolvedUrl, label: label);
final choice = StreamChoice(
url: resolvedUrl,
mimeType: (mime == null || mime.isEmpty) ? guessed : mime,
label: label,
);
candidates.add(choice);
}
if (candidates.isEmpty) return null;
candidates.sort((a, b) => b.score.compareTo(a.score));
StreamChoice? best;
var probeCount = 0;
const maxProbes = 2;
for (final choice in candidates) {
final shouldProbe =
probeCount < maxProbes && _shouldProbeByHeaders(choice);
if (!shouldProbe) {
best = choice;
break;
}
probeCount++;
final probedMime = await probeMimeTypeFromHeaders(
choice.url,
mediaHeaders,
);
if (probedMime == null || probedMime.isEmpty) {
best = choice;
break;
}
if (_isLikelyHtmlContentType(probedMime)) {
AppLogStore.instance.add(
'skip html stream candidate scene=${scene.id} mime=$probedMime label=${choice.label ?? '-'} url=${_shortUrl(choice.url)}',
source: 'stream_resolver',
);
continue;
}
best = StreamChoice(
url: choice.url,
mimeType: probedMime,
label: choice.label,
);
break;
}
best ??= candidates.first;
AppLogStore.instance.add(
'selected stream scene=${scene.id} candidates=${candidates.length} mime=${best.mimeType} label=${best.label ?? '-'} url=${_shortUrl(best.url)}',
source: 'stream_resolver',
);
return best;
}