jellyfin_playback.dart 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import 'package:flutter/foundation.dart';
  2. import '../../core/app_identity.dart';
  3. import 'media_playback.dart';
  4. /// One track in the native Jellyfin playback queue.
  5. @immutable
  6. class JellyfinQueueItem {
  7. const JellyfinQueueItem({
  8. required this.trackId,
  9. required this.mediaSourceId,
  10. required this.url,
  11. required this.title,
  12. required this.artist,
  13. required this.album,
  14. required this.durationMs,
  15. required this.index,
  16. });
  17. final String trackId;
  18. final String mediaSourceId;
  19. final String url;
  20. final String title;
  21. final String artist;
  22. final String album;
  23. final int durationMs;
  24. final int index;
  25. Map<String, Object?> toMap() => {
  26. 'trackId': trackId,
  27. 'mediaSourceId': mediaSourceId,
  28. 'url': url,
  29. 'title': title,
  30. 'artist': artist,
  31. 'album': album,
  32. 'durationMs': durationMs,
  33. 'index': index,
  34. };
  35. }
  36. /// Everything the native service needs to start one album. [authorization] and
  37. /// [accessToken] are kept in memory and are never logged or persisted.
  38. @immutable
  39. class JellyfinPlaybackQueue {
  40. const JellyfinPlaybackQueue({
  41. required this.baseUrl,
  42. required this.authorization,
  43. required this.accessToken,
  44. required this.userId,
  45. required this.deviceId,
  46. required this.playSessionId,
  47. required this.albumId,
  48. required this.albumName,
  49. required this.artistName,
  50. required this.items,
  51. this.artwork,
  52. });
  53. final Uri baseUrl;
  54. final String authorization;
  55. final String accessToken;
  56. final String userId;
  57. final String deviceId;
  58. final String playSessionId;
  59. final String albumId;
  60. final String albumName;
  61. final String artistName;
  62. final List<JellyfinQueueItem> items;
  63. final Uint8List? artwork;
  64. Map<String, Object?> toMap() => {
  65. 'baseUrl': baseUrl.toString(),
  66. 'authorization': authorization,
  67. 'accessToken': accessToken,
  68. 'userId': userId,
  69. 'deviceId': deviceId,
  70. 'playSessionId': playSessionId,
  71. 'albumId': albumId,
  72. 'albumName': albumName,
  73. 'artistName': artistName,
  74. 'artwork': artwork,
  75. 'items': items.map((item) => item.toMap()).toList(growable: false),
  76. };
  77. }
  78. /// The native player's current state. Positions are milliseconds.
  79. @immutable
  80. class JellyfinPlaybackSnapshot {
  81. const JellyfinPlaybackSnapshot({
  82. this.available = false,
  83. this.playing = false,
  84. this.buffering = false,
  85. this.itemId,
  86. this.albumId,
  87. this.title,
  88. this.artist,
  89. this.album,
  90. this.durationMs = 0,
  91. this.positionMs = 0,
  92. this.index = 0,
  93. this.hasPrevious = false,
  94. this.hasNext = false,
  95. this.error,
  96. });
  97. static const empty = JellyfinPlaybackSnapshot();
  98. final bool available;
  99. final bool playing;
  100. final bool buffering;
  101. final String? itemId;
  102. final String? albumId;
  103. final String? title;
  104. final String? artist;
  105. final String? album;
  106. final int durationMs;
  107. final int positionMs;
  108. final int index;
  109. final bool hasPrevious;
  110. final bool hasNext;
  111. final String? error;
  112. bool get playingOrBuffering => playing || buffering;
  113. bool get ended =>
  114. available && !playing && durationMs > 0 && positionMs >= durationMs - 500;
  115. double get progress {
  116. if (durationMs <= 0) return 0;
  117. return (positionMs / durationMs).clamp(0.0, 1.0);
  118. }
  119. JellyfinPlaybackSnapshot copyWith({
  120. bool? available,
  121. bool? playing,
  122. bool? buffering,
  123. int? positionMs,
  124. int? index,
  125. bool? hasPrevious,
  126. bool? hasNext,
  127. String? error,
  128. }) {
  129. return JellyfinPlaybackSnapshot(
  130. available: available ?? this.available,
  131. playing: playing ?? this.playing,
  132. buffering: buffering ?? this.buffering,
  133. itemId: itemId,
  134. albumId: albumId,
  135. title: title,
  136. artist: artist,
  137. album: album,
  138. durationMs: durationMs,
  139. positionMs: positionMs ?? this.positionMs,
  140. index: index ?? this.index,
  141. hasPrevious: hasPrevious ?? this.hasPrevious,
  142. hasNext: hasNext ?? this.hasNext,
  143. error: error ?? this.error,
  144. );
  145. }
  146. factory JellyfinPlaybackSnapshot.fromMap(Map<dynamic, dynamic> map) {
  147. int integer(String key) => (map[key] as num?)?.toInt() ?? 0;
  148. String? string(String key) {
  149. final value = (map[key] as String?)?.trim();
  150. return value == null || value.isEmpty ? null : value;
  151. }
  152. return JellyfinPlaybackSnapshot(
  153. available: map['available'] == true,
  154. playing: map['playing'] == true,
  155. buffering: map['buffering'] == true,
  156. itemId: string('itemId'),
  157. albumId: string('albumId'),
  158. title: string('title'),
  159. artist: string('artist'),
  160. album: string('album'),
  161. durationMs: integer('durationMs'),
  162. positionMs: integer('positionMs'),
  163. index: integer('index'),
  164. hasPrevious: map['hasPrevious'] == true,
  165. hasNext: map['hasNext'] == true,
  166. error: string('error'),
  167. );
  168. }
  169. /// Snapshot consumed by the Home volume card and the existing media widgets.
  170. ///
  171. /// A progressive/transcoded Jellyfin stream often has an unknown duration in
  172. /// ExoPlayer. [fallbackDurationMs] comes from the queued track metadata so
  173. /// the progress ring still has a denominator.
  174. MediaPlayback toMediaPlayback({int fallbackDurationMs = 0}) {
  175. final effectiveDuration = durationMs > 0
  176. ? durationMs
  177. : (fallbackDurationMs > 0 ? fallbackDurationMs : 0);
  178. return MediaPlayback(
  179. hasAccess: available,
  180. hasSession: available && itemId != null,
  181. playing: playing,
  182. title: title,
  183. artist: artist,
  184. packageName: appPackageName,
  185. durationMs: effectiveDuration,
  186. positionMs: positionMs,
  187. );
  188. }
  189. @override
  190. bool operator ==(Object other) {
  191. return other is JellyfinPlaybackSnapshot &&
  192. other.available == available &&
  193. other.playing == playing &&
  194. other.buffering == buffering &&
  195. other.itemId == itemId &&
  196. other.albumId == albumId &&
  197. other.title == title &&
  198. other.artist == artist &&
  199. other.album == album &&
  200. other.durationMs == durationMs &&
  201. other.positionMs == positionMs &&
  202. other.index == index &&
  203. other.hasPrevious == hasPrevious &&
  204. other.hasNext == hasNext &&
  205. other.error == error;
  206. }
  207. @override
  208. int get hashCode => Object.hash(
  209. available,
  210. playing,
  211. buffering,
  212. itemId,
  213. albumId,
  214. title,
  215. artist,
  216. album,
  217. durationMs,
  218. positionMs,
  219. index,
  220. hasPrevious,
  221. hasNext,
  222. error,
  223. );
  224. }