mirror of
https://github.com/immich-app/immich.git
synced 2026-06-27 22:35:57 +00:00
use hls in video viewer
This commit is contained in:
@@ -67,6 +67,9 @@ class URLSessionManager: NSObject {
|
||||
delegate = URLSessionManagerDelegate()
|
||||
session = Self.buildSession(delegate: delegate)
|
||||
super.init()
|
||||
if #available(iOS 15, *) {
|
||||
VideoProxyServer.shared.session = session
|
||||
}
|
||||
Self.serverUrls = UserDefaults.group.stringArray(forKey: SERVER_URLS_KEY) ?? []
|
||||
NotificationCenter.default.addObserver(
|
||||
Self.self,
|
||||
@@ -78,6 +81,9 @@ class URLSessionManager: NSObject {
|
||||
|
||||
func recreateSession() {
|
||||
session = Self.buildSession(delegate: delegate)
|
||||
if #available(iOS 15, *) {
|
||||
VideoProxyServer.shared.session = session
|
||||
}
|
||||
}
|
||||
|
||||
static func setServerUrls(_ urls: [String]) {
|
||||
@@ -249,9 +255,6 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
|
||||
let credential = URLCredential(identity: identity as! SecIdentity,
|
||||
certificates: nil,
|
||||
persistence: .forSession)
|
||||
if #available(iOS 15, *) {
|
||||
VideoProxyServer.shared.session = session
|
||||
}
|
||||
return completion(.useCredential, credential)
|
||||
}
|
||||
completion(.performDefaultHandling, nil)
|
||||
@@ -268,9 +271,6 @@ class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate, URLSessionWeb
|
||||
else {
|
||||
return completion(.performDefaultHandling, nil)
|
||||
}
|
||||
if #available(iOS 15, *) {
|
||||
VideoProxyServer.shared.session = session
|
||||
}
|
||||
let credential = URLCredential(user: user, password: password, persistence: .forSession)
|
||||
completion(.useCredential, credential)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class ServerFeatures {
|
||||
final bool passwordLogin;
|
||||
final bool ocr;
|
||||
final bool smartSearch;
|
||||
final bool realtimeTranscoding;
|
||||
|
||||
const ServerFeatures({
|
||||
required this.trash,
|
||||
@@ -15,6 +16,7 @@ class ServerFeatures {
|
||||
required this.passwordLogin,
|
||||
this.ocr = false,
|
||||
this.smartSearch = false,
|
||||
this.realtimeTranscoding = false,
|
||||
});
|
||||
|
||||
ServerFeatures copyWith({
|
||||
@@ -24,6 +26,7 @@ class ServerFeatures {
|
||||
bool? passwordLogin,
|
||||
bool? ocr,
|
||||
bool? smartSearch,
|
||||
bool? realtimeTranscoding,
|
||||
}) {
|
||||
return ServerFeatures(
|
||||
trash: trash ?? this.trash,
|
||||
@@ -32,12 +35,13 @@ class ServerFeatures {
|
||||
passwordLogin: passwordLogin ?? this.passwordLogin,
|
||||
ocr: ocr ?? this.ocr,
|
||||
smartSearch: smartSearch ?? this.smartSearch,
|
||||
realtimeTranscoding: realtimeTranscoding ?? this.realtimeTranscoding,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)';
|
||||
return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch, realtimeTranscoding: $realtimeTranscoding)';
|
||||
}
|
||||
|
||||
ServerFeatures.fromDto(ServerFeaturesDto dto)
|
||||
@@ -46,7 +50,8 @@ class ServerFeatures {
|
||||
oauthEnabled = dto.oauth,
|
||||
passwordLogin = dto.passwordLogin,
|
||||
ocr = dto.ocr,
|
||||
smartSearch = dto.smartSearch;
|
||||
smartSearch = dto.smartSearch,
|
||||
realtimeTranscoding = dto.realtimeTranscoding;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant ServerFeatures other) {
|
||||
@@ -59,7 +64,8 @@ class ServerFeatures {
|
||||
other.oauthEnabled == oauthEnabled &&
|
||||
other.passwordLogin == passwordLogin &&
|
||||
other.ocr == ocr &&
|
||||
other.smartSearch == smartSearch;
|
||||
other.smartSearch == smartSearch &&
|
||||
other.realtimeTranscoding == realtimeTranscoding;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -69,6 +75,7 @@ class ServerFeatures {
|
||||
oauthEnabled.hashCode ^
|
||||
passwordLogin.hashCode ^
|
||||
ocr.hashCode ^
|
||||
smartSearch.hashCode;
|
||||
smartSearch.hashCode ^
|
||||
realtimeTranscoding.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,14 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/hls.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
|
||||
@@ -46,6 +49,8 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
Timer? _loadTimer;
|
||||
bool _isVideoReady = false;
|
||||
bool _shouldPlayOnForeground = true;
|
||||
String? _sessionId;
|
||||
String? _remoteAssetId;
|
||||
|
||||
VideoPlayerNotifier get _notifier => ref.read(videoPlayerProvider(widget.asset.heroTag).notifier);
|
||||
|
||||
@@ -67,6 +72,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
if (!widget.isCurrent) {
|
||||
_loadTimer?.cancel();
|
||||
_notifier.pause();
|
||||
_endSession();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,6 +85,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_loadTimer?.cancel();
|
||||
_removeListeners();
|
||||
_endSession();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -141,14 +148,22 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
);
|
||||
}
|
||||
|
||||
final remoteId = (videoAsset as RemoteAsset).id;
|
||||
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||
final String videoUrl = videoAsset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
||||
final realtimeTranscoding = ref.read(serverInfoProvider).serverFeatures.realtimeTranscoding;
|
||||
// Motion photo clips are short, so spinning up a transcoding session for them is wasteful
|
||||
final useHls = !isOriginalVideo && videoAsset.livePhotoVideoId == null && realtimeTranscoding;
|
||||
final remoteId = (videoAsset as RemoteAsset).id;
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final String videoUrl;
|
||||
if (useHls) {
|
||||
videoUrl = '$serverEndpoint/assets/$remoteId/video/stream/main.m3u8';
|
||||
} else {
|
||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||
videoUrl = videoAsset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
||||
}
|
||||
_remoteAssetId = remoteId;
|
||||
|
||||
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
||||
} catch (error) {
|
||||
@@ -209,11 +224,42 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
_notifier.onNativeStatusChanged();
|
||||
}
|
||||
|
||||
void _onSourceResolved() {
|
||||
final url = _controller?.onPlaybackSourceResolved.value;
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sessionId = extractHlsSessionId(url);
|
||||
if (sessionId == null || sessionId == _sessionId) {
|
||||
return;
|
||||
}
|
||||
// If the player started a new session without us reloading, end the old one
|
||||
_endSession();
|
||||
_sessionId = sessionId;
|
||||
}
|
||||
|
||||
void _endSession() {
|
||||
final sessionId = _sessionId;
|
||||
final assetId = _remoteAssetId;
|
||||
_sessionId = null;
|
||||
if (sessionId == null || assetId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
ref.read(apiServiceProvider).assetsApi.endSession(assetId, sessionId).onError((error, stackTrace) {
|
||||
_log.warning('Failed to end HLS session $sessionId for asset $assetId', error, stackTrace);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _removeListeners() {
|
||||
_controller?.onPlaybackPositionChanged.removeListener(_onPlaybackPositionChanged);
|
||||
_controller?.onPlaybackStatusChanged.removeListener(_onPlaybackStatusChanged);
|
||||
_controller?.onPlaybackReady.removeListener(_onPlaybackReady);
|
||||
_controller?.onPlaybackEnded.removeListener(_onPlaybackEnded);
|
||||
_controller?.onPlaybackSourceResolved.removeListener(_onSourceResolved);
|
||||
}
|
||||
|
||||
void _loadVideo() async {
|
||||
@@ -244,6 +290,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
nc.onPlaybackStatusChanged.addListener(_onPlaybackStatusChanged);
|
||||
nc.onPlaybackReady.addListener(_onPlaybackReady);
|
||||
nc.onPlaybackEnded.addListener(_onPlaybackEnded);
|
||||
nc.onPlaybackSourceResolved.addListener(_onSourceResolved);
|
||||
|
||||
_controller = nc;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user