feat: Major Upgrade to Video.js v8 — Chapters Functionality, Fixes and Improvements

This commit is contained in:
Yiannis Christodoulou
2025-10-20 15:30:00 +03:00
committed by GitHub
parent b39072c8ae
commit a5e6e7b9ca
362 changed files with 62326 additions and 238721 deletions

View File

@@ -604,7 +604,7 @@ def handle_video_chapters(media, chapters):
else:
video_chapter = models.VideoChapterData.objects.create(media=media, data=chapters)
return media.chapter_data
return {'chapters': media.chapter_data}
def change_media_owner(media_id, new_user):

View File

@@ -630,7 +630,7 @@ class Media(models.Model):
@property
def trim_video_url(self):
if self.media_type not in ["video"]:
if self.media_type not in ["video", "audio"]:
return None
ret = self.encodings.filter(status="success", profile__extension='mp4', chunk=False).order_by("-profile__resolution").first()
@@ -642,7 +642,7 @@ class Media(models.Model):
@property
def trim_video_path(self):
if self.media_type not in ["video"]:
if self.media_type not in ["video", "audio"]:
return None
ret = self.encodings.filter(status="success", profile__extension='mp4', chunk=False).order_by("-profile__resolution").first()

View File

@@ -12,40 +12,19 @@ class VideoChapterData(models.Model):
class Meta:
unique_together = ['media']
def save(self, *args, **kwargs):
from .. import tasks
is_new = self.pk is None
if is_new or (not is_new and self._check_data_changed()):
super().save(*args, **kwargs)
tasks.produce_video_chapters.delay(self.pk)
else:
super().save(*args, **kwargs)
def _check_data_changed(self):
if self.pk:
old_instance = VideoChapterData.objects.get(pk=self.pk)
return old_instance.data != self.data
return False
@property
def chapter_data(self):
# ensure response is consistent
data = []
for item in self.data:
if item.get("start") and item.get("title"):
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = helpers.url_from_path(thumbnail)
else:
thumbnail = "static/images/chapter_default.jpg"
data.append(
{
"start": item.get("start"),
"title": item.get("title"),
"thumbnail": thumbnail,
if self.data and isinstance(self.data, list):
for item in self.data:
if item.get("startTime") and item.get("endTime") and item.get("chapterTitle"):
chapter_item = {
'startTime': item.get("startTime"),
'endTime': item.get("endTime"),
'chapterTitle': item.get("chapterTitle"),
}
)
data.append(chapter_item)
return data

View File

@@ -52,7 +52,6 @@ from .models import (
Subtitle,
Tag,
TranscriptionRequest,
VideoChapterData,
VideoTrimRequest,
)
@@ -950,45 +949,6 @@ def update_encoding_size(encoding_id):
return False
@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
media = chapter_object.media
video_path = media.media_file.path
output_folder = media.video_chapters_folder
chapters = chapter_object.data
width = 336
height = 188
if not os.path.exists(output_folder):
os.makedirs(output_folder)
results = []
for i, chapter in enumerate(chapters):
timestamp = chapter["start"]
title = chapter["title"]
output_filename = f"thumbnail_{i:02d}.jpg" # noqa
output_path = os.path.join(output_folder, output_filename)
command = [settings.FFMPEG_COMMAND, "-y", "-ss", str(timestamp), "-i", video_path, "-vframes", "1", "-q:v", "2", "-s", f"{width}x{height}", output_path]
ret = run_command(command) # noqa
if os.path.exists(output_path) and get_file_type(output_path) == "image":
results.append({"start": timestamp, "title": title, "thumbnail": output_path})
chapter_object.data = results
chapter_object.save(update_fields=["data"])
return True
@task(name="post_trim_action", queue="short_tasks", soft_time_limit=600)
def post_trim_action(friendly_token):
"""Perform post-processing actions after video trimming

View File

@@ -244,8 +244,6 @@ 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("/")
@@ -258,20 +256,26 @@ def video_chapters(request, friendly_token):
return HttpResponseRedirect("/")
try:
data = json.loads(request.body)["chapters"]
request_data = json.loads(request.body)
data = request_data.get("chapters")
if data is None:
return JsonResponse({'success': False, 'error': 'Request must contain "chapters" array'}, status=400)
chapters = []
for _, chapter_data in enumerate(data):
start_time = chapter_data.get('start')
title = chapter_data.get('title')
if start_time and title:
start_time = chapter_data.get('startTime')
end_time = chapter_data.get('endTime')
chapter_title = chapter_data.get('chapterTitle')
if start_time and end_time and chapter_title:
chapters.append(
{
'start': start_time,
'title': title,
'startTime': start_time,
'endTime': end_time,
'chapterTitle': chapter_title,
}
)
except Exception as e: # noqa
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with start and title'}, status=400)
return JsonResponse({'success': False, 'error': 'Request data must be a list of video chapters with startTime, endTime, chapterTitle'}, status=400)
ret = handle_video_chapters(media, chapters)
@@ -358,8 +362,6 @@ 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("/")
@@ -371,10 +373,11 @@ def edit_chapters(request):
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
chapters = media.chapter_data
return render(
request,
"cms/edit_chapters.html",
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token},
{"media_object": media, "add_subtitle_url": media.add_subtitle_url, "media_file_path": helpers.url_from_path(media.media_file.path), "media_id": media.friendly_token, "chapters": chapters},
)
@@ -426,7 +429,7 @@ def edit_video(request):
if not (request.user == media.user or is_mediacms_editor(request.user)):
return HttpResponseRedirect("/")
if not media.media_type == "video":
if media.media_type not in ["video", "audio"]:
messages.add_message(request, messages.INFO, "Media is not video")
return HttpResponseRedirect(media.get_absolute_url())