This commit is contained in:
Markos Gogoulos 2025-05-23 16:25:51 +03:00
parent f39de968c8
commit 1c9aff252e
8 changed files with 49 additions and 34 deletions

View File

@ -14,7 +14,6 @@ from django.core.files import File
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from contextlib import contextmanager
from cms import celery_app from cms import celery_app
@ -23,15 +22,6 @@ from .helpers import mask_ip
logger = logging.getLogger(__name__) 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): def get_user_or_session(request):
"""Return a dictionary with user info """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() add_date=timezone.now()
) )
models.Media.objects.bulk_create([new_media]) 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: 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}" logs=f"Copied from encoding {encoding.id}"
) )
models.Encoding.objects.bulk_create([new_encoding]) models.Encoding.objects.bulk_create([new_encoding])
# avoids calling signals # avoids calling signals as this is still not ready
# Copy categories and tags # Copy categories and tags
for category in original_media.category.all(): for category in original_media.category.all():

View File

@ -526,6 +526,8 @@ class Media(models.Model):
with open(self.media_file.path, "rb") as f: with open(self.media_file.path, "rb") as f:
myfile = File(f) myfile = File(f)
thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg" 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.thumbnail.save(content=myfile, name=thumbnail_name, save=False)
self.poster.save(content=myfile, name=thumbnail_name, save=False) self.poster.save(content=myfile, name=thumbnail_name, save=False)
self.save(update_fields=["thumbnail", "poster"]) self.save(update_fields=["thumbnail", "poster"])
@ -563,6 +565,8 @@ class Media(models.Model):
with open(tf, "rb") as f: with open(tf, "rb") as f:
myfile = File(f) myfile = File(f)
thumbnail_name = helpers.get_file_name(self.media_file.path) + ".jpg" 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.thumbnail.save(content=myfile, name=thumbnail_name, save=False)
self.poster.save(content=myfile, name=thumbnail_name, save=False) self.poster.save(content=myfile, name=thumbnail_name, save=False)
self.save(update_fields=["thumbnail", "poster"]) self.save(update_fields=["thumbnail", "poster"])

View File

@ -62,9 +62,11 @@ ERRORS_LIST = [
def handle_pending_running_encodings(media): def handle_pending_running_encodings(media):
"""Handle pending and running encodings for a media object. """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 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). However for encodings that are in pending and has succeeded, so we can keep them (they will be trimmed) or if we dont keep them we dont have to delete them
or running phase, here
However for encodings that are in pending or running phase, just delete them
Args: Args:
media: The media object to handle encodings for media: The media object to handle encodings for
@ -86,15 +88,23 @@ def handle_pending_running_encodings(media):
def pre_trim_actions(media): def pre_trim_video_actions(media):
# avoid re-running unnecessary encodings (or chunkize_media, which is the first step for them) # the reason for this function is to perform tasks before trimming a video
# 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 # 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) profiles = EncodeProfile.objects.filter(active=True, extension='mp4', resolution__lte=media.video_height)
media_encodings = EncodeProfile.objects.filter( media_encodings = EncodeProfile.objects.filter(
encoding__in=media.encodings.filter( encoding__in=media.encodings.filter(
@ -113,8 +123,8 @@ def pre_trim_actions(media):
if picked: 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") logger.info(f"Encoding media {media.friendly_token} will have to be performed for all profiles")
media.encode() media.encode()
return True return True
@ -244,6 +254,9 @@ def encode_media(
"""Encode a media to given profile, using ffmpeg, storing progress""" """Encode a media to given profile, using ffmpeg, storing progress"""
logger.info(f"encode_media for {friendly_token}/{profile_id}/{encoding_id}/{force}/{chunk}") 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(): 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") logger.info(f"Exiting for {friendly_token}/{profile_id}/{encoding_id}/{force} since encoding id not found")
return False return False
@ -390,6 +403,9 @@ def encode_media(
logger.info("Saved {0}".format(round(percent, 2))) logger.info("Saved {0}".format(round(percent, 2)))
n_times += 1 n_times += 1
except DatabaseError: 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.temp_file)
kill_ffmpeg_process(encoding.chunk_file_path) kill_ffmpeg_process(encoding.chunk_file_path)
return False return False
@ -539,7 +555,6 @@ def create_hls(friendly_token):
if os.path.exists(pp): if os.path.exists(pp):
if media.hls_file != pp: if media.hls_file != pp:
Media.objects.filter(pk=media.pk).update(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 return True
@ -882,6 +897,8 @@ def update_encoding_size(encoding_id):
@task(name="produce_video_chapters", queue="short_tasks") @task(name="produce_video_chapters", queue="short_tasks")
def produce_video_chapters(chapter_id): def produce_video_chapters(chapter_id):
# this is not used
return False
chapter_object = VideoChapterData.objects.filter(id=chapter_id).first() chapter_object = VideoChapterData.objects.filter(id=chapter_id).first()
if not chapter_object: if not chapter_object:
return False return False
@ -999,14 +1016,15 @@ def video_trim_task(self, trim_request_id):
trim_request.media = new_media trim_request.media = new_media
trim_request.save(update_fields=["media"]) trim_request.save(update_fields=["media"])
# processing timestamps differently on encodings and original file, since they have different I-frames # processing timestamps differently on encodings and original file, in case we do accuracy trimming (currently not)
# the cut is made based on the I-frames # 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) original_trim_result = trim_video_method(target_media.media_file.path, timestamps_original)
if not original_trim_result: if not original_trim_result:
logger.info(f"Failed to trim original file for media {target_media.friendly_token}") logger.info(f"Failed to trim original file for media {target_media.friendly_token}")
deleted_encodings = handle_pending_running_encodings(target_media) 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) encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False)
for encoding in encodings: for encoding in encodings:
trim_result = trim_video_method(encoding.media_file.path, timestamps_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}") logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}")
encoding.delete() encoding.delete()
pre_trim_actions(target_media) pre_trim_video_actions(target_media)
post_trim_action.delay(target_media.friendly_token) post_trim_action.delay(target_media.friendly_token)
else: 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]) original_trim_result = trim_video_method(target_media.media_file.path, [timestamp])
deleted_encodings = handle_pending_running_encodings(target_media) 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) encodings = target_media.encodings.filter(status="success", profile__extension='mp4', chunk=False)
for encoding in encodings: for encoding in encodings:
trim_result = trim_video_method(encoding.media_file.path, [timestamp]) 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}") logger.info(f"Failed to trim encoding {encoding.id} for media {target_media.friendly_token}")
encoding.delete() encoding.delete()
pre_trim_actions(target_media) pre_trim_video_actions(target_media)
post_trim_action.delay(target_media.friendly_token) post_trim_action.delay(target_media.friendly_token)

View File

@ -246,6 +246,8 @@ def history(request):
@csrf_exempt @csrf_exempt
@login_required @login_required
def video_chapters(request, friendly_token): def video_chapters(request, friendly_token):
# this is not ready...
return False
if not request.method == "POST": if not request.method == "POST":
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
@ -354,7 +356,8 @@ def publish_media(request):
@login_required @login_required
def edit_chapters(request): def edit_chapters(request):
"""Edit chapters""" """Edit chapters"""
# not implemented yet
return False
friendly_token = request.GET.get("m", "").strip() friendly_token = request.GET.get("m", "").strip()
if not friendly_token: if not friendly_token:
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
@ -614,7 +617,8 @@ def view_media(request):
context["CAN_EDIT_MEDIA"] = True context["CAN_EDIT_MEDIA"] = True
context["CAN_DELETE_COMMENTS"] = 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': if media.media_type == 'video':
video_msg = None video_msg = None
if media.encoding_status == "pending": if media.encoding_status == "pending":

View File

@ -7,7 +7,6 @@ A modern browser-based video editing tool built with React and TypeScript that i
- ⏱️ Trim video start and end points - ⏱️ Trim video start and end points
- ✂️ Split videos into multiple segments - ✂️ Split videos into multiple segments
- 👁️ Preview individual segments or the full edited video - 👁️ Preview individual segments or the full edited video
- 🔍 Zoom timeline for precise editing
- 🔄 Undo/redo support for all editing operations - 🔄 Undo/redo support for all editing operations
- 🔊 Audio mute controls - 🔊 Audio mute controls
- 💾 Save edits directly to MediaCMS - 💾 Save edits directly to MediaCMS

View File

@ -9,8 +9,7 @@
<link href="{% static "video_editor/video-editor.css" %}" rel="stylesheet"> <link href="{% static "video_editor/video-editor.css" %}" rel="stylesheet">
<script> <script>
window.MEDIA_DATA = { window.MEDIA_DATA = {
// videoUrl: "{{ media_file_path }}", // "http://temp.web357.com/SampleVideo_1280x720_30mb.mp4", videoUrl: "",
videoUrl: "http://temp.web357.com/SampleVideo_1280x720_30mb.mp4",
mediaId: "{{ media_id }}", mediaId: "{{ media_id }}",
chapters: [ chapters: [
{ {