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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 582 additions and 178 deletions

View File

@ -3,6 +3,7 @@
<application <application
android:label="Immich" android:label="Immich"
android:name="${applicationName}" android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -0,0 +1,20 @@
// Generated file.
// If you wish to remove Flutter's multidex support, delete this entire file.
package io.flutter.app;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex;
/**
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
*/
public class FlutterMultiDexApplication extends FlutterApplication {
@Override
@CallSuper
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}

View File

@ -17,6 +17,10 @@ PODS:
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
- Toast (4.0.0) - Toast (4.0.0)
- video_player_avfoundation (0.0.1):
- Flutter
- wakelock (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -25,6 +29,8 @@ DEPENDENCIES:
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@ -44,6 +50,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/photo_manager/ios" :path: ".symlinks/plugins/photo_manager/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios"
wakelock:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
@ -54,6 +64,8 @@ SPEC CHECKSUMS:
photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463 photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

View File

@ -41,9 +41,22 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true /> <true />
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>App need your agree, can visit your album</string> <string>We need to manage backup your photos album</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict> </dict>
</plist> </plist>

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -12,6 +13,12 @@ void main() async {
// Hive.registerAdapter(ImmichBackUpAssetAdapter()); // Hive.registerAdapter(ImmichBackUpAssetAdapter());
// Hive.deleteBoxFromDisk(hiveImmichBox); // Hive.deleteBoxFromDisk(hiveImmichBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
),
);
runApp(const ProviderScope(child: ImmichApp())); runApp(const ProviderScope(child: ImmichApp()));
} }
@ -69,6 +76,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.indigo, primarySwatch: Colors.indigo,
textTheme: GoogleFonts.workSansTextTheme( textTheme: GoogleFonts.workSansTextTheme(
Theme.of(context).textTheme.apply(fontSizeFactor: 1.0), Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
@ -79,6 +87,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
foregroundColor: Colors.indigo, foregroundColor: Colors.indigo,
elevation: 1, elevation: 1,
centerTitle: true, centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
), ),
), ),
routeInformationParser: _immichRouter.defaultRouteParser(), routeInformationParser: _immichRouter.defaultRouteParser(),

View File

@ -1,23 +1,50 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart';
class ImageGrid extends StatelessWidget { class ImageGrid extends ConsumerWidget {
final List<ImmichAsset> assetGroup; final List<ImmichAsset> assetGroup;
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key); const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid( return SliverGrid(
gridDelegate: gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5.0, mainAxisSpacing: 5), const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5.0, mainAxisSpacing: 5),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
var assetType = assetGroup[index].type;
return GestureDetector( return GestureDetector(
onTap: () {}, onTap: () {},
child: ThumbnailImage(asset: assetGroup[index]), child: Stack(
); children: [
ThumbnailImage(asset: assetGroup[index]),
assetType == 'IMAGE'
? Container()
: Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
assetGroup[index].duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
)
],
));
}, },
childCount: assetGroup.length, childCount: assetGroup.length,
), ),

View File

@ -1,6 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/asset.provider.dart'; import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
@ -100,14 +99,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
], ],
), ),
], ],
systemOverlayStyle: const SystemUiOverlayStyle(
// Status bar color
statusBarColor: Colors.indigo,
// Status bar brightness (optional)
statusBarIconBrightness: Brightness.light, // For Android (dark icons)
statusBarBrightness: Brightness.dark,
),
), ),
); );
} }

View File

@ -1,39 +1,53 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
class ThumbnailImage extends StatelessWidget { class ThumbnailImage extends HookWidget {
final ImmichAsset asset; final ImmichAsset asset;
const ThumbnailImage({Key? key, required this.asset}) : super(key: key); const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true'; '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
AutoRouter.of(context).push( if (asset.type == 'IMAGE') {
ImageViewerRoute( AutoRouter.of(context).push(
imageUrl: ImageViewerRoute(
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false', imageUrl:
heroTag: asset.id, '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
thumbnailUrl: thumbnailRequestUrl, heroTag: asset.id,
), thumbnailUrl: thumbnailRequestUrl,
); ),
);
} else {
debugPrint("Navigate to video player");
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
),
);
}
}, },
onLongPress: () {}, onLongPress: () {},
child: Hero( child: Hero(
tag: asset.id, tag: asset.id,
child: CachedNetworkImage( child: CachedNetworkImage(
cacheKey: "${asset.id}-${cacheKey.value}",
width: 300, width: 300,
height: 300, height: 300,
memCacheHeight: 250, memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
@ -44,6 +58,7 @@ class ThumbnailImage extends StatelessWidget {
), ),
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
debugPrint("Error Loading Thumbnail Widget $error"); debugPrint("Error Loading Thumbnail Widget $error");
cacheKey.value += 1;
return const Icon(Icons.error); return const Icon(Icons.error);
}, },
), ),

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart'; import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/providers/asset.provider.dart'; import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -16,9 +16,9 @@ class HomePage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final ValueNotifier<bool> _showBackToTopBtn = useState(false); final ValueNotifier<bool> _showBackToTopBtn = useState(false);
ScrollController _scrollController = useScrollController(); ScrollController _scrollController = useScrollController();
List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider); List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
List<Widget> imageGridGroup = []; List<Widget> imageGridGroup = [];
final scrollLabelText = useState("");
_scrollControllerCallback() { _scrollControllerCallback() {
var endOfPage = _scrollController.position.maxScrollExtent; var endOfPage = _scrollController.position.maxScrollExtent;
@ -40,39 +40,10 @@ class HomePage extends HookConsumerWidget {
_scrollController.addListener(_scrollControllerCallback); _scrollController.addListener(_scrollControllerCallback);
return () { return () {
debugPrint("Remove scroll listener");
_scrollController.removeListener(_scrollControllerCallback); _scrollController.removeListener(_scrollControllerCallback);
}; };
}, []); }, []);
SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(dateTitle).year;
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle));
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0),
child: Text(
dateText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
],
),
),
);
}
Widget _buildBody() { Widget _buildBody() {
if (assetGroup.isNotEmpty) { if (assetGroup.isNotEmpty) {
String lastGroupDate = assetGroup[0].date; String lastGroupDate = assetGroup[0].date;
@ -86,44 +57,27 @@ class HomePage extends HookConsumerWidget {
// Add Monthly Title Group if started at the beginning of the month // Add Monthly Title Group if started at the beginning of the month
if ((currentMonth! - previousMonth!) != 0) { if ((currentMonth! - previousMonth!) != 0) {
var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle));
imageGridGroup.add( imageGridGroup.add(
MonthlyTitleText(monthTitleText: monthTitleText), MonthlyTitleText(isoDate: dateTitle),
); );
} }
// Add Daily Title Group // Add Daily Title Group
imageGridGroup.add( imageGridGroup.add(
DailyTitleText(dateTitle: dateTitle), DailyTitleText(isoDate: dateTitle),
); );
// Add Image Group // Add Image Group
imageGridGroup.add( imageGridGroup.add(
ImageGrid(assetGroup: assetGroup), ImageGrid(assetGroup: assetGroup),
); );
//
lastGroupDate = dateTitle; lastGroupDate = dateTitle;
} }
} }
return SafeArea( return SafeArea(
child: DraggableScrollbar.semicircle( child: DraggableScrollbar.semicircle(
// labelTextBuilder: (offset) {
// final int currentItem = _scrollController.hasClients
// ? (_scrollController.offset / _scrollController.position.maxScrollExtent * imageGridGroup.length)
// .floor()
// : 0;
// if (imageGridGroup[currentItem] is MonthlyTitleText) {
// MonthlyTitleText item = imageGridGroup[currentItem] as MonthlyTitleText;
// scrollLabelText.value = item.monthTitleText;
// }
// return Text(scrollLabelText.value);
// },
// labelConstraints: const BoxConstraints.tightFor(width: 200.0, height: 30.0),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
controller: _scrollController, controller: _scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
@ -148,13 +102,15 @@ class HomePage extends HookConsumerWidget {
class MonthlyTitleText extends StatelessWidget { class MonthlyTitleText extends StatelessWidget {
const MonthlyTitleText({ const MonthlyTitleText({
Key? key, Key? key,
required this.monthTitleText, required this.isoDate,
}) : super(key: key); }) : super(key: key);
final String monthTitleText; final String isoDate;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(isoDate));
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 10.0, top: 32), padding: const EdgeInsets.only(left: 10.0, top: 32),
@ -174,17 +130,17 @@ class MonthlyTitleText extends StatelessWidget {
class DailyTitleText extends StatelessWidget { class DailyTitleText extends StatelessWidget {
const DailyTitleText({ const DailyTitleText({
Key? key, Key? key,
required this.dateTitle, required this.isoDate,
}) : super(key: key); }) : super(key: key);
final String dateTitle; final String isoDate;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var currentYear = DateTime.now().year; var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(dateTitle).year; var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle)); var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(

View File

@ -13,7 +13,7 @@ class LoginForm extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController(text: 'testuser@email.com'); final usernameController = useTextEditingController(text: 'testuser@email.com');
final passwordController = useTextEditingController(text: 'password'); final passwordController = useTextEditingController(text: 'password');
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:3000'); final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283');
return Center( return Center(
child: ConstrainedBox( child: ConstrainedBox(

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/modules/home/views/home_page.dart';
import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/auth_guard.dart';
import 'package:immich_mobile/shared/views/backup_controller_page.dart'; import 'package:immich_mobile/shared/views/backup_controller_page.dart';
import 'package:immich_mobile/shared/views/image_viewer_page.dart'; import 'package:immich_mobile/shared/views/image_viewer_page.dart';
import 'package:immich_mobile/shared/views/video_viewer_page.dart';
part 'router.gr.dart'; part 'router.gr.dart';
@ -15,6 +16,7 @@ part 'router.gr.dart';
AutoRoute(page: HomePage, guards: [AuthGuard]), AutoRoute(page: HomePage, guards: [AuthGuard]),
AutoRoute(page: BackupControllerPage, guards: [AuthGuard]), AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]), AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
], ],
) )
class AppRouter extends _$AppRouter { class AppRouter extends _$AppRouter {

View File

@ -42,6 +42,12 @@ class _$AppRouter extends RootStackRouter {
imageUrl: args.imageUrl, imageUrl: args.imageUrl,
heroTag: args.heroTag, heroTag: args.heroTag,
thumbnailUrl: args.thumbnailUrl)); thumbnailUrl: args.thumbnailUrl));
},
VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: VideoViewerPage(key: args.key, videoUrl: args.videoUrl));
} }
}; };
@ -52,7 +58,9 @@ class _$AppRouter extends RootStackRouter {
RouteConfig(BackupControllerRoute.name, RouteConfig(BackupControllerRoute.name,
path: '/backup-controller-page', guards: [authGuard]), path: '/backup-controller-page', guards: [authGuard]),
RouteConfig(ImageViewerRoute.name, RouteConfig(ImageViewerRoute.name,
path: '/image-viewer-page', guards: [authGuard]) path: '/image-viewer-page', guards: [authGuard]),
RouteConfig(VideoViewerRoute.name,
path: '/video-viewer-page', guards: [authGuard])
]; ];
} }
@ -120,3 +128,27 @@ class ImageViewerRouteArgs {
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}'; return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}';
} }
} }
/// generated route for
/// [VideoViewerPage]
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
VideoViewerRoute({Key? key, required String videoUrl})
: super(VideoViewerRoute.name,
path: '/video-viewer-page',
args: VideoViewerRouteArgs(key: key, videoUrl: videoUrl));
static const String name = 'VideoViewerRoute';
}
class VideoViewerRouteArgs {
const VideoViewerRouteArgs({this.key, required this.videoUrl});
final Key? key;
final String videoUrl;
@override
String toString() {
return 'VideoViewerRouteArgs{key: $key, videoUrl: $videoUrl}';
}
}

View File

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

View File

@ -35,7 +35,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
void getBackupInfo() async { void getBackupInfo() async {
_updateServerInfo(); _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) { if (list.isEmpty) {
debugPrint("No Asset On Device"); debugPrint("No Asset On Device");
@ -59,7 +59,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// await PhotoManager.presentLimited(); // await PhotoManager.presentLimited();
// Gather assets info // Gather assets info
List<AssetPathEntity> list = 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) { if (list.isEmpty) {
debugPrint("No Asset On Device - Abort Backup Process"); debugPrint("No Asset On Device - Abort Backup Process");

View File

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

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
class FileHelper { class FileHelper {
static getMimeType(String filePath) { static getMimeType(String filePath) {
debugPrint(filePath);
var fileExtension = p.extension(filePath).split(".")[1]; var fileExtension = p.extension(filePath).split(".")[1];
switch (fileExtension) { switch (fileExtension.toLowerCase()) {
case 'gif': case 'gif':
return {"type": "image", "subType": "gif"}; return {"type": "image", "subType": "gif"};

View File

@ -155,6 +155,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
chewie:
dependency: "direct main"
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@ -527,6 +534,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -653,6 +667,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
provider:
dependency: transitive
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -847,6 +868,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
video_player:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.18"
video_player_android:
dependency: transitive
description:
name: video_player_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.17"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.18"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
visibility_detector: visibility_detector:
dependency: "direct main" dependency: "direct main"
description: description:
@ -854,6 +910,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -898,4 +989,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.15.1 <3.0.0" dart: ">=2.15.1 <3.0.0"
flutter: ">=2.5.0" flutter: ">=2.8.0"

View File

@ -28,6 +28,8 @@ dependencies:
visibility_detector: ^0.2.2 visibility_detector: ^0.2.2
flutter_launcher_icons: "^0.9.2" flutter_launcher_icons: "^0.9.2"
fluttertoast: ^8.0.8 fluttertoast: ^8.0.8
video_player: ^2.2.18
chewie: ^1.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -1,3 +1,4 @@
node_modules/ node_modules/
upload/ upload/
dist/ dist/

View File

@ -12,6 +12,8 @@ services:
command: yarn start:dev command: yarn start:dev
ports: ports:
- "3000:3000" - "3000:3000"
# expose:
# - 3000
volumes: volumes:
- .:/usr/src/app - .:/usr/src/app
- userdata:/usr/src/app/upload - userdata:/usr/src/app/upload
@ -47,6 +49,21 @@ services:
networks: networks:
- immich_network - immich_network
nginx:
container_name: proxy_nginx
image: nginx:latest
volumes:
- ./settings/nginx-conf:/etc/nginx/conf.d
ports:
- 2283:80
- 2284:443
logging:
driver: none
networks:
- immich_network
depends_on:
- server
networks: networks:
immich_network: immich_network:
volumes: volumes:

View File

@ -61,16 +61,17 @@
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.7", "@types/bull": "^3.15.7",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/fluent-ffmpeg": "^2.1.20",
"@types/imagemin": "^8.0.0", "@types/imagemin": "^8.0.0",
"@types/jest": "27.0.2", "@types/jest": "27.0.2",
"@types/lodash": "^4.14.178", "@types/lodash": "^4.14.178",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.6", "@types/passport-jwt": "^3.0.6",
"@types/sharp": "^0.29.5",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"@types/sharp": "^0.29.5",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",

View File

@ -0,0 +1,20 @@
server {
client_max_body_size 50000M;
listen 80;
location / {
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://immich_server:3000;
}
}

View File

@ -9,10 +9,10 @@ import {
Param, Param,
ValidationPipe, ValidationPipe,
StreamableFile, StreamableFile,
Response,
Query, Query,
Logger, Response,
UploadedFile, Headers,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
@ -22,16 +22,22 @@ import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto'; import { CreateAssetDto } from './dto/create-asset.dto';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
import { ImageOptimizeService } from '../../modules/image-optimize/image-optimize.service'; import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { AssetType } from './entities/asset.entity'; import { AssetType } from './entities/asset.entity';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { Response as Res } from 'express';
import { promisify } from 'util';
import { stat } from 'fs';
import { pipeline } from 'stream';
const fileInfo = promisify(stat);
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('asset') @Controller('asset')
export class AssetController { export class AssetController {
constructor( constructor(
private readonly assetService: AssetService, private readonly assetService: AssetService,
private readonly imageOptimizeService: ImageOptimizeService, private readonly assetOptimizeService: AssetOptimizeService,
) {} ) {}
@Post('upload') @Post('upload')
@ -45,7 +51,11 @@ export class AssetController {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype); const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
if (savedAsset && savedAsset.type == AssetType.IMAGE) { if (savedAsset && savedAsset.type == AssetType.IMAGE) {
await this.imageOptimizeService.resizeImage(savedAsset); await this.assetOptimizeService.resizeImage(savedAsset);
}
if (savedAsset && savedAsset.type == AssetType.VIDEO) {
await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
} }
}); });
@ -54,23 +64,81 @@ export class AssetController {
@Get('/file') @Get('/file')
async serveFile( async serveFile(
@Headers() headers,
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res, @Response({ passthrough: true }) res: Res,
@Query(ValidationPipe) query: ServeFileDto, @Query(ValidationPipe) query: ServeFileDto,
): Promise<StreamableFile> { ): Promise<StreamableFile> {
let file = null; let file = null;
const asset = await this.assetService.findOne(authUser, query.did, query.aid); const asset = await this.assetService.findOne(authUser, query.did, query.aid);
res.set({
'Content-Type': asset.mimeType,
});
if (query.isThumb === 'false' || !query.isThumb) { // Handle Sending Images
file = createReadStream(asset.originalPath); if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
} else { res.set({
file = createReadStream(asset.resizePath); 'Content-Type': asset.mimeType,
});
if (query.isThumb === 'false' || !query.isThumb) {
file = createReadStream(asset.originalPath);
} else {
file = createReadStream(asset.resizePath);
}
return new StreamableFile(file);
} else if (asset.type == AssetType.VIDEO) {
// Handle Handling Video
const { size } = await fileInfo(asset.originalPath);
const range = headers.range;
if (range) {
/** Extracting Start and End value from Range Header */
let [start, end] = range.replace(/bytes=/, '').split('-');
start = parseInt(start, 10);
end = end ? parseInt(end, 10) : size - 1;
if (!isNaN(start) && isNaN(end)) {
start = start;
end = size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = size - end;
end = size - 1;
}
// Handle unavailable range request
if (start >= size || end >= size) {
console.error('Bad Request');
// Return the 416 Range Not Satisfiable.
res.status(416).set({
'Content-Range': `bytes */${size}`,
});
throw new BadRequestException('Bad Request Range');
}
/** Sending Partial Content With HTTP Code 206 */
console.log('Sendinf file with type ', asset.mimeType);
res.status(206).set({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': asset.mimeType,
});
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else {
res.set({
'Content-Type': asset.mimeType,
});
return new StreamableFile(createReadStream(asset.originalPath));
}
} }
return new StreamableFile(file); console.log('SHOULD NOT BE HERE');
} }
@Get('/all') @Get('/all')

View File

@ -4,13 +4,13 @@ import { AssetController } from './asset.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from './entities/asset.entity'; import { AssetEntity } from './entities/asset.entity';
import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module'; import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module';
import { ImageOptimizeService } from '../../modules/image-optimize/image-optimize.service'; import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
@Module({ @Module({
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
name: 'image', name: 'optimize',
defaultJobOptions: { defaultJobOptions: {
attempts: 3, attempts: 3,
removeOnComplete: true, removeOnComplete: true,
@ -29,7 +29,7 @@ import { BullModule } from '@nestjs/bull';
ImageOptimizeModule, ImageOptimizeModule,
], ],
controllers: [AssetController], controllers: [AssetController],
providers: [AssetService, ImageOptimizeService], providers: [AssetService, AssetOptimizeService],
exports: [], exports: [],
}) })
export class AssetModule {} export class AssetModule {}

View File

@ -26,9 +26,9 @@ export class AssetService {
asset.createdAt = assetInfo.createdAt; asset.createdAt = assetInfo.createdAt;
asset.modifiedAt = assetInfo.modifiedAt; asset.modifiedAt = assetInfo.modifiedAt;
asset.isFavorite = assetInfo.isFavorite; asset.isFavorite = assetInfo.isFavorite;
asset.lat = assetInfo.lat;
asset.lon = assetInfo.lon;
asset.mimeType = mimeType; asset.mimeType = mimeType;
asset.duration = assetInfo.duration;
try { try {
const res = await this.assetRepository.save(asset); const res = await this.assetRepository.save(asset);
@ -63,7 +63,7 @@ export class AssetService {
lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(), lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
}) })
.orderBy('a."createdAt"::date', 'DESC') .orderBy('a."createdAt"::date', 'DESC')
.take(10000) // .take(500)
.getMany(); .getMany();
if (assets.length > 0) { if (assets.length > 0) {

View File

@ -24,8 +24,5 @@ export class CreateAssetDto {
fileExtension: string; fileExtension: string;
@IsOptional() @IsOptional()
lat: string; duration: string;
@IsOptional()
lon: string;
} }

View File

@ -33,17 +33,11 @@ export class AssetEntity {
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
isFavorite: boolean; isFavorite: boolean;
@Column({ nullable: true })
description: string;
@Column({ nullable: true })
lat: string;
@Column({ nullable: true })
lon: string;
@Column({ nullable: true }) @Column({ nullable: true })
mimeType: string; mimeType: string;
@Column({ nullable: true })
duration: string;
} }
export enum AssetType { export enum AssetType {

View File

@ -1,7 +1,6 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import ffmpeg from 'fluent-ffmpeg';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);

View File

@ -6,13 +6,13 @@ import { AssetModule } from '../../api-v1/asset/asset.module';
import { AssetService } from '../../api-v1/asset/asset.service'; import { AssetService } from '../../api-v1/asset/asset.service';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { ImageOptimizeProcessor } from './image-optimize.processor'; import { ImageOptimizeProcessor } from './image-optimize.processor';
import { ImageOptimizeService } from './image-optimize.service'; import { AssetOptimizeService } from './image-optimize.service';
import { MachineLearningProcessor } from './machine-learning.processor'; import { MachineLearningProcessor } from './machine-learning.processor';
@Module({ @Module({
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
name: 'image', name: 'optimize',
defaultJobOptions: { defaultJobOptions: {
attempts: 3, attempts: 3,
removeOnComplete: true, removeOnComplete: true,
@ -30,7 +30,7 @@ import { MachineLearningProcessor } from './machine-learning.processor';
TypeOrmModule.forFeature([AssetEntity]), TypeOrmModule.forFeature([AssetEntity]),
], ],
providers: [ImageOptimizeService, ImageOptimizeProcessor, MachineLearningProcessor], providers: [AssetOptimizeService, ImageOptimizeProcessor, MachineLearningProcessor],
exports: [ImageOptimizeService], exports: [AssetOptimizeService],
}) })
export class ImageOptimizeModule {} export class ImageOptimizeModule {}

View File

@ -6,9 +6,10 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import sharp from 'sharp'; import sharp from 'sharp';
import fs, { existsSync, mkdirSync } from 'fs'; import fs, { existsSync, mkdirSync } from 'fs';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'crypto'; import ffmpeg from 'fluent-ffmpeg';
import { Logger } from '@nestjs/common';
@Processor('image') @Processor('optimize')
export class ImageOptimizeProcessor { export class ImageOptimizeProcessor {
constructor( constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@ -16,8 +17,8 @@ export class ImageOptimizeProcessor {
private configService: ConfigService, private configService: ConfigService,
) {} ) {}
@Process('optimize') @Process('resize-image')
async handleOptimization(job: Job) { async resizeUploadedImage(job: Job) {
const { savedAsset }: { savedAsset: AssetEntity } = job.data; const { savedAsset }: { savedAsset: AssetEntity } = job.data;
const basePath = this.configService.get('UPLOAD_LOCATION'); const basePath = this.configService.get('UPLOAD_LOCATION');
@ -58,4 +59,32 @@ export class ImageOptimizeProcessor {
return 'ok'; return 'ok';
} }
@Process('get-video-thumbnail')
async resizeUploadedVideo(job: Job) {
const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
const basePath = this.configService.get('UPLOAD_LOCATION');
// const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
console.log(filename);
// Create folder for thumb image if not exist
const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
if (!existsSync(resizeDir)) {
mkdirSync(resizeDir, { recursive: true });
}
ffmpeg(savedAsset.originalPath)
.thumbnail({
count: 1,
timestamps: [1],
folder: resizeDir,
filename: `${filename}.png`,
})
.on('end', async (a) => {
await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
});
return 'ok';
}
} }

View File

@ -2,17 +2,15 @@ import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Queue } from 'bull'; import { Queue } from 'bull';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { join } from 'path';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
@Injectable() @Injectable()
export class ImageOptimizeService { export class AssetOptimizeService {
constructor(@InjectQueue('image') private imageQueue: Queue) {} constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
public async resizeImage(savedAsset: AssetEntity) { public async resizeImage(savedAsset: AssetEntity) {
const job = await this.imageQueue.add( const job = await this.optimizeQueue.add(
'optimize', 'resize-image',
{ {
savedAsset, savedAsset,
}, },
@ -23,4 +21,19 @@ export class ImageOptimizeService {
jobId: job.id, jobId: job.id,
}; };
} }
public async getVideoThumbnail(savedAsset: AssetEntity, filename: String) {
const job = await this.optimizeQueue.add(
'get-video-thumbnail',
{
savedAsset,
filename,
},
{ jobId: randomUUID() },
);
return {
jobId: job.id,
};
}
} }