fix(mobile): play the server copy when the local video file cannot be read (#31606)
* play the server copy when the local video file cannot be read * flatten the video source choice and add tests for it * drop the platform override from the video viewer test
This commit is contained in:
@@ -7,13 +7,13 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
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/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/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
@@ -39,14 +39,15 @@ class NativeVideoViewer extends ConsumerStatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<NativeVideoViewer> createState() => _NativeVideoViewerState();
|
||||
ConsumerState<NativeVideoViewer> createState() => NativeVideoViewerState();
|
||||
}
|
||||
|
||||
class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with WidgetsBindingObserver {
|
||||
class NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with WidgetsBindingObserver {
|
||||
static final _log = Logger('NativeVideoViewer');
|
||||
|
||||
NativeVideoPlayerController? _controller;
|
||||
late final Future<VideoSource?> _videoSource;
|
||||
@visibleForTesting
|
||||
late final Future<VideoSource?> videoSource;
|
||||
Timer? _loadTimer;
|
||||
bool _isVideoReady = false;
|
||||
bool _shouldPlayOnForeground = true;
|
||||
@@ -57,7 +58,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_videoSource = _createSource();
|
||||
videoSource = _createSource();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -113,6 +114,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
}
|
||||
|
||||
try {
|
||||
final storageRepository = ref.read(storageRepositoryProvider);
|
||||
final localFilePath = widget.localFilePath;
|
||||
if (localFilePath != null) {
|
||||
final file = File(localFilePath);
|
||||
@@ -132,23 +134,26 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
|
||||
if (localAsset != null) {
|
||||
final file = localAsset.isMotionPhoto
|
||||
? await StorageRepository().getMotionFileForAsset(localAsset)
|
||||
: await StorageRepository().getFileForAsset(localAsset.id);
|
||||
? await storageRepository.getMotionFileForAsset(localAsset)
|
||||
: await storageRepository.getFileForAsset(localAsset.id);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
|
||||
// Pass a file:// URI so Android's Uri.parse doesn't
|
||||
// interpret characters like '#' as fragment identifiers.
|
||||
return await VideoSource.init(
|
||||
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
if (file != null) {
|
||||
return await VideoSource.init(
|
||||
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
|
||||
type: VideoSourceType.file,
|
||||
);
|
||||
}
|
||||
|
||||
if (videoAsset is! RemoteAsset) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
_log.warning('Local file missing for ${videoAsset.name} (${videoAsset.localId}), playing the remote copy');
|
||||
}
|
||||
|
||||
final remoteAsset = videoAsset as RemoteAsset;
|
||||
@@ -276,7 +281,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
return;
|
||||
}
|
||||
|
||||
final source = await _videoSource;
|
||||
final source = await videoSource;
|
||||
if (source == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:native_video_player/native_video_player.dart';
|
||||
|
||||
import '../../../infrastructure/repository.mock.dart';
|
||||
import '../../../unit/factories/local_asset_factory.dart';
|
||||
import '../../../unit/factories/remote_asset_factory.dart';
|
||||
import '../../../unit/presentation/presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late MockStorageRepository storage;
|
||||
|
||||
final local = LocalAssetFactory.create(id: 'local-1').copyWith(type: .video, playbackStyle: .video);
|
||||
final remote = RemoteAssetFactory.create(type: .video, localId: local.id);
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
storage = MockStorageRepository();
|
||||
final assets = context.service.asset.service;
|
||||
when(() => assets.getAsset(remote)).thenAnswer((_) async => remote);
|
||||
when(() => assets.getAsset(local)).thenAnswer((_) async => local);
|
||||
when(() => assets.getLocalAsset(local.id)).thenAnswer((_) async => local);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await context.dispose();
|
||||
});
|
||||
|
||||
Future<VideoSource?> pumpViewer(WidgetTester tester, BaseAsset asset) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
NativeVideoViewer(asset: asset, image: const SizedBox()),
|
||||
overrides: [storageRepositoryProvider.overrideWithValue(storage)],
|
||||
expectSettle: false,
|
||||
);
|
||||
return tester.state<NativeVideoViewerState>(find.byType(NativeVideoViewer)).videoSource;
|
||||
}
|
||||
|
||||
testWidgets('plays the local file when it exists', (tester) async {
|
||||
final file = File('/videos/local-1.mp4');
|
||||
when(() => storage.getFileForAsset(local.id)).thenAnswer((_) async => file);
|
||||
|
||||
final source = await pumpViewer(tester, remote);
|
||||
|
||||
expect(source?.type, VideoSourceType.file);
|
||||
expect(source?.path, endsWith(file.path));
|
||||
});
|
||||
|
||||
testWidgets('plays the server copy when the local file cannot be read', (tester) async {
|
||||
when(() => storage.getFileForAsset(local.id)).thenAnswer((_) async => null);
|
||||
|
||||
final source = await pumpViewer(tester, remote);
|
||||
|
||||
expect(source?.type, VideoSourceType.network);
|
||||
expect(source?.path, endsWith('/assets/${remote.id}/video/playback'));
|
||||
});
|
||||
|
||||
testWidgets('gives up when a local only asset has no file', (tester) async {
|
||||
when(() => storage.getFileForAsset(local.id)).thenAnswer((_) async => null);
|
||||
|
||||
final source = await pumpViewer(tester, local);
|
||||
|
||||
expect(source, isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user