diff --git a/files/methods.py b/files/methods.py index f579d256..11667c9d 100644 --- a/files/methods.py +++ b/files/methods.py @@ -14,7 +14,6 @@ from django.core.files import File from django.core.mail import EmailMessage from django.db.models import Q from django.utils import timezone -from contextlib import contextmanager from cms import celery_app @@ -23,15 +22,6 @@ from .helpers import mask_ip logger = logging.getLogger(__name__) -@contextmanager -def disable_signal(signal, receiver, sender): - """Context manager to temporarily disable a signal""" - signal.disconnect(receiver, sender=sender) - try: - yield - finally: - signal.connect(receiver, sender=sender) - def get_user_or_session(request): """Return a dictionary with user info @@ -459,7 +449,7 @@ def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"): add_date=timezone.now() ) models.Media.objects.bulk_create([new_media]) - # avoids calling signals + # avoids calling signals since signals will call media_init and we don't want that if copy_encodings: @@ -477,7 +467,7 @@ def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"): logs=f"Copied from encoding {encoding.id}" ) models.Encoding.objects.bulk_create([new_encoding]) - # avoids calling signals + # avoids calling signals as this is still not ready # Copy categories and tags for category in original_media.category.all(): diff --git a/files/models.py b/files/models.py index 61dc0746..fae9445d 100644 --- a/files/models.py +++ b/files/models.py @@ -526,6 +526,8 @@ class Media(models.Model): with open(self.media_file.path, "rb") as f: myfile = File(f) thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg" + # avoid saving the whole object, because something might have been changed + # on the meanwhile self.thumbnail.save(content=myfile, name=thumbnail_name, save=False) self.poster.save(content=myfile, name=thumbnail_name, save=False) self.save(update_fields=["thumbnail", "poster"]) @@ -563,6 +565,8 @@ class Media(models.Model): with open(tf, "rb") as f: myfile = File(f) thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg" + # avoid saving the whole object, because something might have been changed + # on the meanwhile self.thumbnail.save(content=myfile, name=thumbnail_name, save=False) self.poster.save(content=myfile, name=thumbnail_name, save=False) self.save(update_fields=["thumbnail", "poster"]) diff --git a/files/tasks.py b/files/tasks.py index 9aef1744..7529bfd3 100644 --- a/files/tasks.py +++ b/files/tasks.py @@ -62,9 +62,11 @@ ERRORS_LIST = [ def handle_pending_running_encodings(media): """Handle pending and running encodings for a media object. - we are trimming the original file. If there are encodings in success, this means that the encoding has run - and has succeeded, so we can keep them (they will be trimmed). However for encodings that are in pending - or running phase, + we are trimming the original file. If there are encodings in success state, this means that the encoding has run + and has succeeded, so we can keep them (they will be trimmed) or if we dont keep them we dont have to delete them + here + + However for encodings that are in pending or running phase, just delete them Args: media: The media object to handle encodings for @@ -86,15 +88,23 @@ def handle_pending_running_encodings(media): -def pre_trim_actions(media): - # avoid re-running unnecessary encodings (or chunkize_media, which is the first step for them) - # if the video is already completed. however if it is a new video (user uploded the video and starts trimming - # before the video is processed), this is necessary, so encode has to be called +def pre_trim_video_actions(media): + # the reason for this function is to perform tasks before trimming a video + + # avoid re-running unnecessary encodings (or chunkize_media, which is the first step for them) + # if the video is already completed + # however if it is a new video (user uploded the video and starts trimming + # before the video is processed), this is necessary, so encode has to be called to give it a chance to encode + + # if a video is fully processed (all encodings are success), or if a video is new, then things are clear + + # HOWEVER there is a race condition and this is that some encodings are success and some are pending/running + # Since we are making speed cutting, we will perform an ffmpeg -c copy on all of them and the result will be + # that they will end up differently cut, because ffmpeg checks for I-frames + # The result is fine if playing the video but is bad in case of HLS + # So we need to delete all encodings inevitably to produce same results, if there are some that are success and some that + # are still not finished. - # also since we are making speed cutting, if a video resolution (say 720 and 360) has been ffmpeg copied by the - # original file, it has specificy information as I-frames. Now the original file was trimmed too. So now if we attempt - # to trim it for a missing resolution (eg 240), it will pick different I-frames and the result will be different - # while playing the video in HLS. Thus we need to re-encode the video for all resolutions to ensure they have the same information profiles = EncodeProfile.objects.filter(active=True, extension='mp4', resolution__lte=media.video_height) media_encodings = EncodeProfile.objects.filter( encoding__in=media.encodings.filter( @@ -113,8 +123,8 @@ def pre_trim_actions(media): if picked: + # by calling encode will re-encode all. The logic is explained above... logger.info(f"Encoding media {media.friendly_token} will have to be performed for all profiles") - media.encode() return True @@ -244,6 +254,9 @@ def encode_media( """Encode a media to given profile, using ffmpeg, storing progress""" logger.info(f"encode_media for {friendly_token}/{profile_id}/{encoding_id}/{force}/{chunk}") + # TODO: this is new behavior, check whether it performs well. Before that check it would end up saving the Encoding + # at some point below. Now it exits the task. Could it be that before it would give it a chance to re-run? Or it was + # not being used at all? if not Encoding.objects.filter(id=encoding_id).exists(): logger.info(f"Exiting for {friendly_token}/{profile_id}/{encoding_id}/{force} since encoding id not found") return False @@ -390,6 +403,9 @@ def encode_media( logger.info("Saved {0}".format(round(percent, 2))) n_times += 1 except DatabaseError: + # primary reason for this is that the encoding has been deleted, because + # the media file was deleted, or also that there was a trim video request + # so it would be redundant to let it complete the encoding kill_ffmpeg_process(encoding.temp_file) kill_ffmpeg_process(encoding.chunk_file_path) return False @@ -539,7 +555,6 @@ def create_hls(friendly_token): if os.path.exists(pp): if media.hls_file != pp: Media.objects.filter(pk=media.pk).update(hls_file=pp) - hlsfile = Media.objects.filter(pk=media.pk).first().hls_file return True @@ -882,6 +897,8 @@ def update_encoding_size(encoding_id): @task(name="produce_video_chapters", queue="short_tasks") def produce_video_chapters(chapter_id): + # this is not used + return False chapter_object = VideoChapterData.objects.filter(id=chapter_id).first() if not chapter_object: return False @@ -999,14 +1016,15 @@ def video_trim_task(self, trim_request_id): trim_request.media = new_media trim_request.save(update_fields=["media"]) - # processing timestamps differently on encodings and original file, since they have different I-frames - # the cut is made based on the I-frames + # processing timestamps differently on encodings and original file, in case we do accuracy trimming (currently not) + # these have different I-frames and the cut is made based on the I-frames original_trim_result = trim_video_method(target_media.media_file.path, timestamps_original) if not original_trim_result: logger.info(f"Failed to trim original file for media {target_media.friendly_token}") deleted_encodings = handle_pending_running_encodings(target_media) + # the following could be un-necessary, read commend in pre_trim_video_actions to see why encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False) for encoding in encodings: trim_result = trim_video_method(encoding.media_file.path, timestamps_encodings) @@ -1014,7 +1032,7 @@ def video_trim_task(self, trim_request_id): logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}") encoding.delete() - pre_trim_actions(target_media) + pre_trim_video_actions(target_media) post_trim_action.delay(target_media.friendly_token) else: @@ -1034,6 +1052,7 @@ def video_trim_task(self, trim_request_id): original_trim_result = trim_video_method(target_media.media_file.path, [timestamp]) deleted_encodings = handle_pending_running_encodings(target_media) + # the following could be un-necessary, read commend in pre_trim_video_actions to see why encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False) for encoding in encodings: trim_result = trim_video_method(encoding.media_file.path, [timestamp]) @@ -1041,7 +1060,7 @@ def video_trim_task(self, trim_request_id): logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}") encoding.delete() - pre_trim_actions(target_media) + pre_trim_video_actions(target_media) post_trim_action.delay(target_media.friendly_token) diff --git a/files/views.py b/files/views.py index 363c2651..3555580a 100644 --- a/files/views.py +++ b/files/views.py @@ -246,6 +246,8 @@ def history(request): @csrf_exempt @login_required def video_chapters(request, friendly_token): + # this is not ready... + return False if not request.method == "POST": return HttpResponseRedirect("/") @@ -354,7 +356,8 @@ def publish_media(request): @login_required def edit_chapters(request): """Edit chapters""" - + # not implemented yet + return False friendly_token = request.GET.get("m", "").strip() if not friendly_token: return HttpResponseRedirect("/") @@ -614,7 +617,8 @@ def view_media(request): context["CAN_EDIT_MEDIA"] = True context["CAN_DELETE_COMMENTS"] = True - # TODO: explaim + # in case media is video and is processing (eg the case a video was just uploaded) + # attempt to show it (rather than showing a blank video player) if media.media_type == 'video': video_msg = None if media.encoding_status == "pending": diff --git a/frontend-tools/video-editor/README.md b/frontend-tools/video-editor/README.md index 8418289b..ddb91455 100644 --- a/frontend-tools/video-editor/README.md +++ b/frontend-tools/video-editor/README.md @@ -7,7 +7,6 @@ A modern browser-based video editing tool built with React and TypeScript that i - ⏱️ Trim video start and end points - ✂️ Split videos into multiple segments - 👁️ Preview individual segments or the full edited video -- 🔍 Zoom timeline for precise editing - 🔄 Undo/redo support for all editing operations - 🔊 Audio mute controls - 💾 Save edits directly to MediaCMS diff --git a/frontend-tools/video-editor/client/public/videos/sample-video-30s.mp4 b/frontend-tools/video-editor/client/public/videos/sample-video-30s.mp4 deleted file mode 100644 index 851c175c..00000000 Binary files a/frontend-tools/video-editor/client/public/videos/sample-video-30s.mp4 and /dev/null differ diff --git a/frontend-tools/video-editor/client/public/videos/sample-video-37s.mp4 b/frontend-tools/video-editor/client/public/videos/sample-video-37s.mp4 deleted file mode 100644 index 819250e7..00000000 Binary files a/frontend-tools/video-editor/client/public/videos/sample-video-37s.mp4 and /dev/null differ diff --git a/templates/cms/edit_chapters.html b/templates/cms/edit_chapters.html index 31c9829f..c9c6c557 100644 --- a/templates/cms/edit_chapters.html +++ b/templates/cms/edit_chapters.html @@ -9,8 +9,7 @@