Skip to content

Commit

Permalink
Fix error when creating a movie that includes videos from before v1.5
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleKun committed Sep 2, 2023
1 parent 1113b6c commit b1046e0
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Fixed saving video error when color parsing fails
- Fixed calendar page resetting to current date after returning from save video page for past dates
- Fixed videoplayer showing some landscape videos rotated in preview
- Fixed movie creation that includes videos from before v1.5
- Fixed movie deletion not updating the movie list correctly

## v1.5.1 - 02/2023
Expand Down
86 changes: 52 additions & 34 deletions lib/pages/home/create_movie/widgets/create_movie_button.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';

import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -45,6 +46,7 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {

void _createMovie() async {
WakelockPlus.enable();
final List<String> copiesToDelete = [];

setState(() {
isProcessing = true;
Expand Down Expand Up @@ -109,8 +111,6 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {
String videosFolder = SharedPrefsUtil.getString('appPath');
if (currentProfileName.isNotEmpty) {
videosFolder = '${videosFolder}Profiles/$currentProfileName/';
} else {
videosFolder = '$videosFolder';
}

Utils.logInfo('${logTag}Base videos folder: $videosFolder');
Expand All @@ -119,10 +119,17 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {
final String dummySubtitles = await Utils.writeSrt('', 0);

// Start checking all videos
int currentIndex = 0;
for (String video in selectedVideos) {
currentIndex++;
bool isV1point5 = true;
final String currentVideo = '$videosFolder$video';
final String tempVideo = '${currentVideo.split('.mp4').first}_temp.mp4';

// I hate MediaStore
final String randomNumber1 = Random().nextInt(1000000).toString();
final String randomNumber2 = Random().nextInt(1000000).toString();
final String tempVideo1 = '${currentVideo.split('.mp4').first}_$randomNumber1.mp4';
final String tempVideo2 = '${currentVideo.split('.mp4').first}_$randomNumber2.mp4';

// TODO(KyleKun): this (in special) will need a good refactor for next version
// Check if video was recorded before v1.5 so we can process what is needed
Expand All @@ -146,60 +153,66 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {
});

// Make sure all selected videos have a subtitles and audio stream before creating movie, and finally check their resolution, resizes if necessary.
// To avoid asking permission for every single video, we make a copy and leave the original untouched
if (!isV1point5) {
// Replace the entry in the list with the processed copy
final copyVideoName =
'${selectedVideos[selectedVideos.indexOf(video)].split('.mp4').first}_$randomNumber1.mp4';
selectedVideos[selectedVideos.indexOf(video)] = copyVideoName;
copiesToDelete.add(tempVideo1);

// Make sure it is 1080p, h264
// Also set the framerate to 30 and copy all the streams
await executeFFmpeg(
'-i "$currentVideo" -vf "scale=1920:1080" -r 30 -map 0 -c:v libx264 -c:a copy -c:s copy -crf 20 -preset slow "$tempVideo" -y')
'-i "$currentVideo" -vf "scale=1920:1080" -r 30 -map 0 -c:v libx264 -c:a copy -c:s copy -crf 20 -preset slow "$tempVideo1" -y')
.then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
StorageUtils.deleteFile(currentVideo);
StorageUtils.renameFile(tempVideo, currentVideo);
Utils.logInfo('${logTag}Converted $currentVideo to 1080p, h264');
Utils.logInfo(
'${logTag}Copied $currentVideo to $tempVideo1 and converted it to 1080p, h264');
} else {
final sessionLog = await session.getLogsAsString();
Utils.logError('${logTag}Error converting $currentVideo to 1080p, h264');
Utils.logError('${logTag}Error: $sessionLog');
}
});

Utils.logInfo('${logTag}Checking streams for $currentVideo');
Utils.logInfo('${logTag}Checking streams for $tempVideo1');
bool hasSubtitles = false;
bool hasAudio = false;

// Streams check
await executeFFprobe(
'-v quiet -print_format json -show_format -show_streams "$currentVideo"')
'-v quiet -print_format json -show_format -show_streams "$tempVideo1"')
.then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
final sessionLog = await session.getOutput();
if (sessionLog == null) return;
final List<dynamic> streams = jsonDecode(sessionLog)['streams'];
debugPrint('${logTag}Streams info for $currentVideo --> $sessionLog');
debugPrint('${logTag}Streams info for $tempVideo1 --> $sessionLog');
for (var stream in streams) {
if (stream['codec_type'] == 'audio') {
Utils.logWarning('$logTag$currentVideo already has audio!');
Utils.logWarning('$logTag$tempVideo1 already has audio!');
// Make sure the audio stream is mono
await executeFFmpeg(
'-i "$currentVideo" -map 0 -c:v copy -c:a aac -ac 1 -ar 48000 -b:a 256k -c:s copy "$tempVideo" -y')
'-i "$tempVideo1" -map 0 -c:v copy -c:a aac -ac 1 -ar 48000 -b:a 256k -c:s copy "$tempVideo2" -y')
.then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
StorageUtils.deleteFile(currentVideo);
StorageUtils.renameFile(tempVideo, currentVideo);
StorageUtils.deleteFile(tempVideo1);
StorageUtils.renameFile(tempVideo2, tempVideo1);
Utils.logInfo('${logTag}Made sure $currentVideo is mono');
} else {
final sessionLog = await session.getLogsAsString();
Utils.logError('${logTag}Error converting $currentVideo to mono audio');
Utils.logError('${logTag}Error converting $tempVideo1 to mono audio');
Utils.logError('${logTag}Error: $sessionLog');
}
});
hasAudio = true;
}
if (stream['codec_type'] == 'subtitle') {
Utils.logWarning('$logTag$currentVideo already has subtitles!');
Utils.logWarning('$logTag$tempVideo1 already has subtitles!');
hasSubtitles = true;
}
}
Expand All @@ -208,65 +221,65 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {

// Add audio stream if necessary
if (!hasAudio) {
Utils.logInfo('${logTag}No audio stream for $currentVideo, adding one...');
Utils.logInfo('${logTag}No audio stream for $tempVideo1, adding one...');

// Creates an empty audio stream that matches video duration
// Set the audio bitrate to 256k and sample rate to 48k (aac codec)
final command =
'-i "$currentVideo" -f lavfi -i anullsrc=channel_layout=mono:sample_rate=48000 -shortest -b:a 256k -c:v copy -c:s copy -c:a aac "$tempVideo" -y';
'-i "$tempVideo1" -f lavfi -i anullsrc=channel_layout=mono:sample_rate=48000 -shortest -b:a 256k -c:v copy -c:s copy -c:a aac "$tempVideo2" -y';
await executeFFmpeg(command).then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
StorageUtils.deleteFile(currentVideo);
StorageUtils.renameFile(tempVideo, currentVideo);
Utils.logInfo('${logTag}Added empty audio stream to $currentVideo');
StorageUtils.deleteFile(tempVideo1);
StorageUtils.renameFile(tempVideo2, tempVideo1);
Utils.logInfo('${logTag}Added empty audio stream to $tempVideo1');
} else {
final sessionLog = await session.getLogsAsString();
Utils.logError('${logTag}Error adding audio stream to $currentVideo');
Utils.logError('${logTag}Error adding audio stream to $tempVideo1');
Utils.logError('${logTag}Error: $sessionLog');
}
});
}

// Add subtitles stream if necessary
if (!hasSubtitles) {
Utils.logInfo('${logTag}No subtitles stream for $currentVideo, adding one...');
Utils.logInfo('${logTag}No subtitles stream for $tempVideo1, adding one...');
final command =
'-i "$currentVideo" -i $dummySubtitles -c copy -c:s mov_text "$tempVideo" -y';
'-i "$tempVideo1" -i $dummySubtitles -c copy -c:s mov_text "$tempVideo2" -y';
await executeFFmpeg(command).then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
StorageUtils.deleteFile(currentVideo);
StorageUtils.renameFile(tempVideo, currentVideo);
Utils.logInfo('${logTag}Added empty subtitles stream to $currentVideo');
StorageUtils.deleteFile(tempVideo1);
StorageUtils.renameFile(tempVideo2, tempVideo1);
Utils.logInfo('${logTag}Added empty subtitles stream to $tempVideo1');
} else {
final sessionLog = await session.getLogsAsString();
Utils.logError('${logTag}Error adding subtitles stream to $currentVideo');
Utils.logError('${logTag}Error adding subtitles stream to $tempVideo1');
Utils.logError('${logTag}Error: $sessionLog');
}
});
}

// Add artist metadata to avoid redoing all that in this video in the future since it was already processed
await executeFFmpeg(
'-i "$currentVideo" -metadata artist="${Constants.artist}" -metadata album="Default" -metadata comment="origin=osd_recording_old" -c:v copy -c:a copy -c:s copy "$tempVideo" -y')
'-i "$tempVideo1" -metadata artist="${Constants.artist}" -metadata album="Default" -metadata comment="origin=osd_recording_old" -c:v copy -c:a copy -c:s copy "$tempVideo2" -y')
.then((session) async {
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
StorageUtils.deleteFile(currentVideo);
StorageUtils.renameFile(tempVideo, currentVideo);
Utils.logInfo('${logTag}Added artist metadata to $currentVideo');
StorageUtils.deleteFile(tempVideo1);
StorageUtils.renameFile(tempVideo2, tempVideo1);
Utils.logInfo('${logTag}Added artist metadata to $tempVideo1');
} else {
final sessionLog = await session.getLogsAsString();
Utils.logError('${logTag}Error adding artist metadata to $currentVideo');
Utils.logError('${logTag}Error adding artist metadata to $tempVideo1');
Utils.logError('${logTag}Error: $sessionLog');
}
});
}

if (mounted) {
setState(() {
progress = '${selectedVideos.indexOf(video) + 1} / ${selectedVideos.length}';
progress = '$currentIndex / ${selectedVideos.length}';
});
} else {
Utils.logWarning('${logTag}Aborted movie creation!');
Expand Down Expand Up @@ -361,6 +374,11 @@ class _CreateMovieButtonState extends State<CreateMovieButton> {
);
} finally {
WakelockPlus.disable();
Utils.logInfo('Deleting temp files... : $copiesToDelete');
copiesToDelete.forEach((copy) {
StorageUtils.deleteFile(copy);
});

setState(() {
isProcessing = false;
});
Expand Down
58 changes: 22 additions & 36 deletions lib/utils/storage_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ class StorageUtils {
/// Create application folder in internal storage
static Future<void> createFolder() async {
// Set internal appDirectoryPath
final io.Directory internalDirectoryPath =
await getApplicationDocumentsDirectory();
final io.Directory internalDirectoryPath = await getApplicationDocumentsDirectory();
SharedPrefsUtil.putString(
'internalDirectoryPath',
internalDirectoryPath.path,
Expand Down Expand Up @@ -97,33 +96,28 @@ class StorageUtils {
}

// Migrate old videos to new folder inside DCIM if needed
final io.Directory oldAppFolder = io.Directory(
SharedPrefsUtil.getString('appPath')
.replaceFirst('DCIM/OneSecondDiary/', 'OneSecondDiary/'));
final io.Directory oldMoviesFolder = io.Directory(
SharedPrefsUtil.getString('moviesPath')
.replaceFirst('DCIM/OneSecondDiary/Movies/', 'OSD-Movies/'));
final io.Directory oldAppFolder = io.Directory(SharedPrefsUtil.getString('appPath')
.replaceFirst('DCIM/OneSecondDiary/', 'OneSecondDiary/'));
final io.Directory oldMoviesFolder = io.Directory(SharedPrefsUtil.getString('moviesPath')
.replaceFirst('DCIM/OneSecondDiary/Movies/', 'OSD-Movies/'));

if (await oldAppFolder.exists()) {
// Map all files inside old folders
final oldFolderFiles =
await oldAppFolder.list(recursive: true).toList();
final oldFolderFiles = await oldAppFolder.list(recursive: true).toList();

// Remove files that contain Logs in path
oldFolderFiles.removeWhere((file) => file.path.contains('Logs'));

// Avoid repeating it if the migration was already done and user forgot to delete old folder
final newFolderFiles =
await appDirectory.list(recursive: true).toList();
final newFolderFiles = await appDirectory.list(recursive: true).toList();
List<String> alreadyMigratedFiles = [];
if (newFolderFiles.length >= oldFolderFiles.length) {
Utils.logInfo(
'[StorageUtils] - Old videos folder already migrated',
);
return;
} else if (newFolderFiles.isNotEmpty) {
alreadyMigratedFiles =
newFolderFiles.map((file) => file.path.split('/').last).toList();
alreadyMigratedFiles = newFolderFiles.map((file) => file.path.split('/').last).toList();
debugPrint('Already migrated files: $alreadyMigratedFiles');
}

Expand Down Expand Up @@ -189,18 +183,15 @@ class StorageUtils {
// Create the profile folder
final folderName = pathSplitted[pathSplitted.length - 2];
MediaStore.appFolder = 'OneSecondDiary/Profiles/$folderName';
final tempFolderPath =
'${internalDirectoryPath.path}/Profiles/$folderName/';
final tempFolderPath = '${internalDirectoryPath.path}/Profiles/$folderName/';
await io.Directory(tempFolderPath).create(recursive: true);
final newFolderPath =
'${SharedPrefsUtil.getString('appPath')}Profiles/$folderName/';
final newFolderPath = '${SharedPrefsUtil.getString('appPath')}Profiles/$folderName/';
await io.Directory(newFolderPath).create(recursive: true);
debugPrint('Created profile folder $newFolderPath');

// Update shared prefs
// Get profiles from persistence
List<String>? storedProfiles =
SharedPrefsUtil.getStringList('profiles');
List<String>? storedProfiles = SharedPrefsUtil.getStringList('profiles');
if (storedProfiles == null || storedProfiles.isEmpty) {
// Add the default profile to storage
storedProfiles = ['Default'];
Expand Down Expand Up @@ -240,8 +231,7 @@ class StorageUtils {
!alreadyMigratedFiles.contains(file.path.split('/').last)) {
MediaStore.appFolder = 'OneSecondDiary';
validFiles++;
final copyFile =
'${internalDirectoryPath.path}/${file.path.split('/').last}';
final copyFile = '${internalDirectoryPath.path}/${file.path.split('/').last}';
await file.copy(copyFile);
await mediaStorePlugin
.saveFile(
Expand All @@ -267,18 +257,15 @@ class StorageUtils {
// If copying videos succeeded, then delete old folder and proceed to movies migration
if (validFiles == copiedFiles) {
if (await oldMoviesFolder.exists()) {
final oldMoviesFolderFiles =
await oldMoviesFolder.list(recursive: true).toList();
final oldMoviesFolderFiles = await oldMoviesFolder.list(recursive: true).toList();
debugPrint(oldMoviesFolderFiles.toString());
// Copy all movies files to new folder
await Future.forEach(oldMoviesFolderFiles, (file) async {
if (file is io.File && file.path.endsWith('.mp4')) {
MediaStore.appFolder = 'OneSecondDiary/Movies';
final tempFolderPath =
'${internalDirectoryPath.path}/Movies/';
final tempFolderPath = '${internalDirectoryPath.path}/Movies/';
await io.Directory(tempFolderPath).create(recursive: true);
final copyFile =
'$tempFolderPath${file.path.split('/').last}';
final copyFile = '$tempFolderPath${file.path.split('/').last}';
await file.copy(copyFile);
await mediaStorePlugin
.saveFile(
Expand Down Expand Up @@ -398,8 +385,7 @@ class StorageUtils {
// Create log file in internal storage
static Future<void> cleanOldLogFiles() async {
try {
final String logsPath =
'${SharedPrefsUtil.getString('internalDirectoryPath')}/Logs';
final String logsPath = '${SharedPrefsUtil.getString('internalDirectoryPath')}/Logs';

final io.Directory logsDirectory = io.Directory(logsPath);

Expand All @@ -414,8 +400,7 @@ class StorageUtils {
final int difference = today.difference(fileDate).inDays;

if (difference > 7) {
Utils.logInfo(
'[StorageUtils] - ' + 'Deleted old log file: ${file.path}');
Utils.logInfo('[StorageUtils] - ' + 'Deleted old log file: ${file.path}');
await io.File(file.path).delete();
}
}
Expand Down Expand Up @@ -459,7 +444,6 @@ class StorageUtils {
}
}

/// Rename file
static void renameFile(String oldPath, String newPath) {
if (checkFileExists(oldPath)) {
try {
Expand All @@ -470,15 +454,17 @@ class StorageUtils {
}
}

/// Used to check if daily video was already recorded
static bool checkFileExists(String filePath) {
return io.File(filePath).existsSync();
}

/// Delete old video if user is editing daily entry
static void deleteFile(String filePath) {
if (checkFileExists(filePath)) {
io.File(filePath).deleteSync(recursive: true);
try {
io.File(filePath).deleteSync(recursive: true);
} catch (e) {
Utils.logError('[StorageUtils] - $e');
}
}
}
}

0 comments on commit b1046e0

Please sign in to comment.