import '../media/media_file_info.dart'; import '../media/media_source_info.dart'; import '../media/media_version.dart'; import '../models/plex/plex_video_playback_data.dart'; import '../utils/json_utils.dart'; import '../utils/plex_url_helper.dart'; import 'file_info_parser.dart'; import 'plex_mappers.dart'; const _streamReader = PlexFileInfoStreamReader(); PlexVideoPlaybackData parsePlexVideoPlaybackDataFromJson( Map? metadataJson, { required String baseUrl, required String? token, int mediaIndex = 0, void Function(int requestedIndex, int fallbackIndex)? onVersionFallback, }) { String? videoUrl; MediaSourceInfo? mediaInfo; List availableVersions = []; final markers = plexMarkersFromCacheJson(metadataJson); if (metadataJson != null) { if (metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) { final mediaList = metadataJson['Media'] as List; availableVersions = mediaList .map((media) => PlexMappers.mediaVersionFromJson(media as Map)) .toList(); if (mediaIndex < 0 || mediaIndex >= mediaList.length) { mediaIndex = 0; } if (!availableVersions[mediaIndex].isPlayable) { final fallback = availableVersions.indexWhere((v) => v.isPlayable); if (fallback >= 0) { onVersionFallback?.call(mediaIndex, fallback); mediaIndex = fallback; } } final media = mediaList[mediaIndex]; if (media['Part'] != null && (media['Part'] as List).isNotEmpty) { final part = media['Part'][0]; final partKey = part['key'] as String?; if (partKey != null) { videoUrl = '$baseUrl$partKey'.withPlexToken(token); final streams = walkStreams(part['Stream'] as List?, _streamReader); final chapters = plexChaptersFromCacheJson(metadataJson); mediaInfo = MediaSourceInfo( videoUrl: videoUrl, audioTracks: streams.audioTracks, subtitleTracks: streams.subtitleTracks, chapters: chapters, partId: part['id'] as int?, displayCriteria: PlexMappers.displayCriteriaFromJson(media as Map?, streams.videoStream), videoAspectRatio: ((media as Map?)?['aspectRatio'] as num?)?.toDouble(), ); } } } } return PlexVideoPlaybackData( videoUrl: videoUrl, mediaInfo: mediaInfo, availableVersions: availableVersions, markers: markers, ); } MediaFileInfo? parsePlexFileInfoFromJson(Map? metadataJson) { if (metadataJson != null && metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) { final media = metadataJson['Media'][0]; final part = media['Part'] != null && (media['Part'] as List).isNotEmpty ? media['Part'][0] : null; // One pass over the streams array, capturing both the raw video / audio // map pointers (for fields the parsed track classes don't carry — // colorSpace, bitDepth, …) and the parsed track lists. final parsedTracks = walkStreams(part?['Stream'] as List?, _streamReader); final videoStream = parsedTracks.videoStream; final audioStream = parsedTracks.audioStream; return MediaFileInfo( // Media level properties container: media['container'] as String?, videoCodec: media['videoCodec'] as String?, videoResolution: media['videoResolution'] as String?, videoFrameRate: media['videoFrameRate'] as String?, videoProfile: media['videoProfile'] as String?, width: media['width'] as int?, height: media['height'] as int?, aspectRatio: (media['aspectRatio'] as num?)?.toDouble(), bitrate: media['bitrate'] as int?, duration: media['duration'] as int?, audioCodec: media['audioCodec'] as String?, audioProfile: media['audioProfile'] as String?, audioChannels: media['audioChannels'] as int?, optimizedForStreaming: flexibleBool(media['optimizedForStreaming']), has64bitOffsets: flexibleBool(media['has64bitOffsets']), // Part level properties (file) filePath: part?['file'] as String?, fileSize: part?['size'] as int?, // Video stream details colorSpace: videoStream?['colorSpace'] as String?, colorRange: videoStream?['colorRange'] as String?, colorPrimaries: videoStream?['colorPrimaries'] as String?, chromaSubsampling: videoStream?['chromaSubsampling'] as String?, frameRate: (videoStream?['frameRate'] as num?)?.toDouble(), bitDepth: videoStream?['bitDepth'] as int?, videoBitrate: videoStream?['bitrate'] as int?, // Audio stream details audioChannelLayout: audioStream?['audioChannelLayout'] as String?, // All audio and subtitle tracks audioTracks: parsedTracks.audioTracks, subtitleTracks: parsedTracks.subtitleTracks, ); } return null; }