resolvePreferredStream method

Future<StreamChoice?> resolvePreferredStream(
  1. Scene scene
)

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;
}