Implemented Video Upload and Player (#2)

* Implementing video upload features

* setup image resize processor

* Add video thumbnail with duration and icon

* Fixed issue with video upload timeout and upper case file type on ios

* Added video player page

* Added video player page

* Fixing video player not play on ios

* Added partial file streaming for ios/android video request

* Added nginx as proxy server for better file serving

* update nginx and docker-compose file

* Video player working correctly

* Video player working correctly

* Split duration to the second
This commit is contained in:
Alex
2022-02-06 00:07:56 -06:00
committed by GitHub
parent b6a7d40863
commit 97dc7660b4
32 changed files with 582 additions and 178 deletions

View File

@@ -5,26 +5,22 @@ class ImmichAsset {
final String deviceAssetId;
final String userId;
final String deviceId;
final String assetType;
final String localPath;
final String remotePath;
final String type;
final String createdAt;
final String modifiedAt;
final bool isFavorite;
final String? description;
final String? duration;
ImmichAsset({
required this.id,
required this.deviceAssetId,
required this.userId,
required this.deviceId,
required this.assetType,
required this.localPath,
required this.remotePath,
required this.type,
required this.createdAt,
required this.modifiedAt,
required this.isFavorite,
this.description,
this.duration,
});
ImmichAsset copyWith({
@@ -32,26 +28,22 @@ class ImmichAsset {
String? deviceAssetId,
String? userId,
String? deviceId,
String? assetType,
String? localPath,
String? remotePath,
String? type,
String? createdAt,
String? modifiedAt,
bool? isFavorite,
String? description,
String? duration,
}) {
return ImmichAsset(
id: id ?? this.id,
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
userId: userId ?? this.userId,
deviceId: deviceId ?? this.deviceId,
assetType: assetType ?? this.assetType,
localPath: localPath ?? this.localPath,
remotePath: remotePath ?? this.remotePath,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
modifiedAt: modifiedAt ?? this.modifiedAt,
isFavorite: isFavorite ?? this.isFavorite,
description: description ?? this.description,
duration: duration ?? this.duration,
);
}
@@ -61,13 +53,11 @@ class ImmichAsset {
'deviceAssetId': deviceAssetId,
'userId': userId,
'deviceId': deviceId,
'assetType': assetType,
'localPath': localPath,
'remotePath': remotePath,
'type': type,
'createdAt': createdAt,
'modifiedAt': modifiedAt,
'isFavorite': isFavorite,
'description': description,
'duration': duration,
};
}
@@ -77,13 +67,11 @@ class ImmichAsset {
deviceAssetId: map['deviceAssetId'] ?? '',
userId: map['userId'] ?? '',
deviceId: map['deviceId'] ?? '',
assetType: map['assetType'] ?? '',
localPath: map['localPath'] ?? '',
remotePath: map['remotePath'] ?? '',
type: map['type'] ?? '',
createdAt: map['createdAt'] ?? '',
modifiedAt: map['modifiedAt'] ?? '',
isFavorite: map['isFavorite'] ?? false,
description: map['description'],
duration: map['duration'],
);
}
@@ -93,7 +81,7 @@ class ImmichAsset {
@override
String toString() {
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, assetType: $assetType, localPath: $localPath, remotePath: $remotePath, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, description: $description)';
return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, type: $type, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, duration: $duration)';
}
@override
@@ -105,13 +93,11 @@ class ImmichAsset {
other.deviceAssetId == deviceAssetId &&
other.userId == userId &&
other.deviceId == deviceId &&
other.assetType == assetType &&
other.localPath == localPath &&
other.remotePath == remotePath &&
other.type == type &&
other.createdAt == createdAt &&
other.modifiedAt == modifiedAt &&
other.isFavorite == isFavorite &&
other.description == description;
other.duration == duration;
}
@override
@@ -120,12 +106,10 @@ class ImmichAsset {
deviceAssetId.hashCode ^
userId.hashCode ^
deviceId.hashCode ^
assetType.hashCode ^
localPath.hashCode ^
remotePath.hashCode ^
type.hashCode ^
createdAt.hashCode ^
modifiedAt.hashCode ^
isFavorite.hashCode ^
description.hashCode;
duration.hashCode;
}
}

View File

@@ -35,7 +35,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
void getBackupInfo() async {
_updateServerInfo();
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.image);
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.common);
if (list.isEmpty) {
debugPrint("No Asset On Device");
@@ -59,7 +59,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// await PhotoManager.presentLimited();
// Gather assets info
List<AssetPathEntity> list =
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.image);
await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
if (list.isEmpty) {
debugPrint("No Asset On Device - Abort Backup Process");

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -12,7 +13,6 @@ import 'package:immich_mobile/utils/files_helper.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;
import 'package:exif/exif.dart';
class BackupService {
final NetworkService _networkService = NetworkService();
@@ -36,7 +36,11 @@ class BackupService {
for (var entity in assetList) {
try {
file = await entity.file.timeout(const Duration(seconds: 5));
if (entity.type == AssetType.video) {
file = await entity.file;
} else {
file = await entity.file.timeout(const Duration(seconds: 5));
}
if (file != null) {
// reading exif
@@ -50,8 +54,8 @@ class BackupService {
String originalFileName = await entity.titleAsync;
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
var fileExtension = p.extension(file.path);
LatLng coordinate = await entity.latlngAsync();
var mimeType = FileHelper.getMimeType(file.path);
var formData = FormData.fromMap({
'deviceAssetId': entity.id,
'deviceId': deviceId,
@@ -60,8 +64,7 @@ class BackupService {
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
'isFavorite': entity.isFavorite,
'fileExtension': fileExtension,
'lat': coordinate.latitude,
'lon': coordinate.longitude,
'duration': entity.videoDuration,
'files': [
await MultipartFile.fromFile(
file.path,

View File

@@ -0,0 +1,105 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:chewie/chewie.dart';
import 'package:video_player/video_player.dart';
class VideoViewerPage extends StatelessWidget {
final String videoUrl;
const VideoViewerPage({Key? key, required this.videoUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
leading: IconButton(
onPressed: () {
AutoRouter.of(context).pop();
},
icon: const Icon(Icons.arrow_back_ios)),
),
body: Center(
child: VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
),
),
);
}
}
class VideoThumbnailPlayer extends StatefulWidget {
final String url;
final String? jwtToken;
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken}) : super(key: key);
@override
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
}
class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
late VideoPlayerController videoPlayerController;
ChewieController? chewieController;
@override
void initState() {
super.initState();
initializePlayer();
}
Future<void> initializePlayer() async {
try {
videoPlayerController =
VideoPlayerController.network(widget.url, httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
await videoPlayerController.initialize();
_createChewieController();
setState(() {});
} catch (e) {
debugPrint("ERROR initialize video player");
print(e);
}
}
_createChewieController() {
chewieController = ChewieController(
showOptions: true,
showControlsOnInitialize: false,
videoPlayerController: videoPlayerController,
autoPlay: true,
autoInitialize: false,
);
}
@override
void dispose() {
super.dispose();
videoPlayerController.pause();
videoPlayerController.dispose();
chewieController?.dispose();
}
@override
Widget build(BuildContext context) {
return chewieController != null && chewieController!.videoPlayerController.value.isInitialized
? SizedBox(
child: Chewie(
controller: chewieController!,
),
)
: const SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
}