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.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():

View File

@ -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"])

View File

@ -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)

View File

@ -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":

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
- ✂️ 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

View File

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