diff --git a/Makefile b/Makefile index 033ffd6196..7a63337100 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,4 @@ dev: docker-compose -f ./server/docker-compose.yml up dev-update: - docker-compose -f ./server/docker-compose.yml up --build -V \ No newline at end of file + docker-compose -f ./server/docker-compose.yml up --build -V diff --git a/mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java new file mode 100644 index 0000000000..9213f130fc --- /dev/null +++ b/mobile/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java @@ -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); + } +} diff --git a/mobile/lib/modules/home/ui/image_grid.dart b/mobile/lib/modules/home/ui/image_grid.dart index e5e0411e6e..470520aa30 100644 --- a/mobile/lib/modules/home/ui/image_grid.dart +++ b/mobile/lib/modules/home/ui/image_grid.dart @@ -1,6 +1,8 @@ +import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart'; import 'package:immich_mobile/shared/models/immich_asset.model.dart'; +import 'package:video_player/video_player.dart'; class ImageGrid extends StatelessWidget { final List assetGroup; @@ -14,9 +16,13 @@ class ImageGrid extends StatelessWidget { const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5.0, mainAxisSpacing: 5), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { + var assetType = assetGroup[index].type; + return GestureDetector( onTap: () {}, - child: ThumbnailImage(asset: assetGroup[index]), + child: assetType == 'IMAGE' + ? ThumbnailImage(asset: assetGroup[index]) + : VideoThumbnailPlayer(key: Key(assetGroup[index].id), videoAsset: assetGroup[index]), ); }, childCount: assetGroup.length, @@ -24,3 +30,56 @@ class ImageGrid extends StatelessWidget { ); } } + +class VideoThumbnailPlayer extends StatefulWidget { + ImmichAsset videoAsset; + + VideoThumbnailPlayer({Key? key, required this.videoAsset}) : super(key: key); + + @override + State createState() => _VideoThumbnailPlayerState(); +} + +class _VideoThumbnailPlayerState extends State { + late VideoPlayerController videoPlayerController; + ChewieController? chewieController; + + @override + void initState() { + super.initState(); + initializePlayer(); + } + + Future initializePlayer() async { + videoPlayerController = + VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'); + + await Future.wait([ + videoPlayerController.initialize(), + ]); + _createChewieController(); + setState(() {}); + } + + _createChewieController() { + chewieController = ChewieController( + showControlsOnInitialize: false, + videoPlayerController: videoPlayerController, + autoPlay: true, + looping: true, + ); + } + + @override + Widget build(BuildContext context) { + return chewieController != null && chewieController!.videoPlayerController.value.isInitialized + ? SizedBox( + height: 300, + width: 300, + child: Chewie( + controller: chewieController!, + ), + ) + : const Text("Loading Video"); + } +} diff --git a/mobile/lib/shared/models/immich_asset.model.dart b/mobile/lib/shared/models/immich_asset.model.dart index 85c0f89a72..8dad845c24 100644 --- a/mobile/lib/shared/models/immich_asset.model.dart +++ b/mobile/lib/shared/models/immich_asset.model.dart @@ -5,9 +5,7 @@ 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; @@ -18,9 +16,7 @@ class ImmichAsset { 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, @@ -32,9 +28,7 @@ class ImmichAsset { String? deviceAssetId, String? userId, String? deviceId, - String? assetType, - String? localPath, - String? remotePath, + String? type, String? createdAt, String? modifiedAt, bool? isFavorite, @@ -45,9 +39,7 @@ class ImmichAsset { 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, @@ -61,9 +53,7 @@ class ImmichAsset { 'deviceAssetId': deviceAssetId, 'userId': userId, 'deviceId': deviceId, - 'assetType': assetType, - 'localPath': localPath, - 'remotePath': remotePath, + 'type': type, 'createdAt': createdAt, 'modifiedAt': modifiedAt, 'isFavorite': isFavorite, @@ -77,9 +67,7 @@ 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, @@ -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, description: $description)'; } @override @@ -105,9 +93,7 @@ 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 && @@ -120,9 +106,7 @@ class ImmichAsset { deviceAssetId.hashCode ^ userId.hashCode ^ deviceId.hashCode ^ - assetType.hashCode ^ - localPath.hashCode ^ - remotePath.hashCode ^ + type.hashCode ^ createdAt.hashCode ^ modifiedAt.hashCode ^ isFavorite.hashCode ^ diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5b51a0e06a..dad4be5230 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -155,6 +155,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + chewie: + dependency: "direct main" + description: + name: chewie + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" cli_util: dependency: transitive description: @@ -527,6 +534,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" octo_image: dependency: transitive description: @@ -653,6 +667,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" pub_semver: dependency: transitive description: @@ -847,6 +868,41 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: "direct main" description: @@ -854,6 +910,41 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -898,4 +989,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.15.1 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index abee88f574..abf1410ac6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -28,6 +28,8 @@ dependencies: visibility_detector: ^0.2.2 flutter_launcher_icons: "^0.9.2" fluttertoast: ^8.0.8 + video_player: ^2.2.18 + chewie: ^1.2.2 dev_dependencies: flutter_test: diff --git a/server/Dockerfile b/server/Dockerfile index 4375f94978..5dc33baf0c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -15,7 +15,8 @@ RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \ rsync \ software-properties-common \ unzip \ - wget + wget \ + ffmpeg # Install NodeJS RUN curl --silent --location https://deb.nodesource.com/setup_14.x | bash - @@ -54,7 +55,8 @@ RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \ rsync \ software-properties-common \ unzip \ - wget + wget \ + ffmpeg # Install NodeJS RUN curl --silent --location https://deb.nodesource.com/setup_14.x | bash - diff --git a/server/Dockerfile-minimal b/server/Dockerfile-minimal index cbe50de964..5df93e35ab 100644 --- a/server/Dockerfile-minimal +++ b/server/Dockerfile-minimal @@ -1,3 +1,6 @@ +################################## +# DEVELOPMENT +################################## FROM node:16-bullseye-slim AS development ARG DEBIAN_FRONTEND=noninteractive @@ -7,7 +10,7 @@ WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN apt-get update -RUN apt-get install gcc g++ make cmake python3 python3-pip -y +RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y RUN npm i -g yarn --force @@ -17,6 +20,18 @@ COPY . . RUN yarn build +# Clean up commands +RUN apt-get autoremove -y && apt-get clean && \ + rm -rf /usr/local/src/* + +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* + + + +################################## +# PRODUCTION +################################## FROM node:16-bullseye-slim as production ARG DEBIAN_FRONTEND=noninteractive ARG NODE_ENV=production @@ -27,7 +42,7 @@ WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN apt-get update -RUN apt-get install gcc g++ make cmake python3 python3-pip -y +RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y RUN npm i -g yarn --force @@ -37,4 +52,12 @@ COPY . . COPY --from=development /usr/src/app/dist ./dist +# Clean up commands +RUN apt-get autoremove -y && apt-get clean && \ + rm -rf /usr/local/src/* + +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* + + CMD ["node", "dist/main"] \ No newline at end of file diff --git a/server/package.json b/server/package.json index 3e65681c2f..b758704676 100644 --- a/server/package.json +++ b/server/package.json @@ -36,12 +36,12 @@ "@tensorflow/tfjs-converter": "^3.13.0", "@tensorflow/tfjs-core": "^3.13.0", "@tensorflow/tfjs-node": "^3.13.0", - "@types/sharp": "^0.29.5", "bcrypt": "^5.0.1", "bull": "^4.4.0", "class-transformer": "^0.5.1", "class-validator": "^0.13.2", "dotenv": "^14.2.0", + "fluent-ffmpeg": "^2.1.2", "joi": "^17.5.0", "lodash": "^4.17.21", "passport": "^0.5.2", @@ -61,12 +61,14 @@ "@types/bcrypt": "^5.0.0", "@types/bull": "^3.15.7", "@types/express": "^4.17.13", + "@types/fluent-ffmpeg": "^2.1.20", "@types/imagemin": "^8.0.0", "@types/jest": "27.0.2", "@types/lodash": "^4.14.178", "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/passport-jwt": "^3.0.6", + "@types/sharp": "^0.29.5", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index ff6c0daafc..6df61a422b 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -48,7 +48,7 @@ export class AssetController { await this.assetOptimizeService.resizeImage(savedAsset); } - if (savedAsset && savedAsset.type == AssetType.IMAGE) { + if (savedAsset && savedAsset.type == AssetType.VIDEO) { await this.assetOptimizeService.resizeVideo(savedAsset); } }); diff --git a/server/src/modules/image-optimize/image-optimize.processor.ts b/server/src/modules/image-optimize/image-optimize.processor.ts index 2f3496d13e..026cb68840 100644 --- a/server/src/modules/image-optimize/image-optimize.processor.ts +++ b/server/src/modules/image-optimize/image-optimize.processor.ts @@ -6,7 +6,8 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; import sharp from 'sharp'; import fs, { existsSync, mkdirSync } from 'fs'; import { ConfigService } from '@nestjs/config'; -import { randomUUID } from 'crypto'; +import ffmpeg from 'fluent-ffmpeg'; +import { Logger } from '@nestjs/common'; @Processor('optimize') export class ImageOptimizeProcessor { @@ -73,30 +74,19 @@ export class ImageOptimizeProcessor { mkdirSync(resizeDir, { recursive: true }); } - fs.readFile(savedAsset.originalPath, (err, data) => { - if (err) { - console.error('Error Reading File'); - } - - sharp(data) - .resize(512, 512, { fit: 'outside' }) - .toFile(resizePath, async (err, info) => { - if (err) { - console.error('Error resizing file ', err); - } - - await this.assetRepository.update(savedAsset, { resizePath: resizePath }); - - // Send file to object detection after resizing - // const detectionJob = await this.machineLearningQueue.add( - // 'object-detection', - // { - // resizePath, - // }, - // { jobId: randomUUID() }, - // ); - }); - }); + ffmpeg(savedAsset.originalPath) + .output(resizePath) + .noAudio() + .videoCodec('libx264') + .size('640x?') + .aspect('4:3') + .on('error', (e) => { + Logger.log(`Error resizing File: ${e}`, 'resizeUploadedVideo'); + }) + .on('end', async () => { + await this.assetRepository.update(savedAsset, { resizePath: resizePath }); + }) + .run(); return 'ok'; } diff --git a/server/yarn.lock b/server/yarn.lock index 834a2322dd..37be4c6066 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1039,6 +1039,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/fluent-ffmpeg@^2.1.20": + version "2.1.20" + resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz#3b5f42fc8263761d58284fa46ee6759a64ce54ac" + integrity sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg== + dependencies: + "@types/node" "*" + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" @@ -1739,6 +1746,11 @@ asap@^2.0.0: resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +async@>=0.2.9: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -3094,6 +3106,14 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== +fluent-ffmpeg@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74" + integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ= + dependencies: + async ">=0.2.9" + which "^1.1.1" + follow-redirects@^1.14.4: version "1.14.7" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz" @@ -6467,6 +6487,13 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"