diff --git a/pod/main/test_settings.py b/pod/main/test_settings.py index f10520f09d..6caf7725ea 100644 --- a/pod/main/test_settings.py +++ b/pod/main/test_settings.py @@ -41,4 +41,5 @@ POD_ARCHIVE_AFFILIATION = ['faculty'] WARN_DEADLINES = [60, 30, 7] USE_BBB = True -USE_VIDEO_RECORD = True \ No newline at end of file +USE_VIDEO_RECORD = True + diff --git a/pod/recorder/plugins/type_audiovideocast.py b/pod/recorder/plugins/type_audiovideocast.py index 29600fb2fd..1fca0989a7 100644 --- a/pod/recorder/plugins/type_audiovideocast.py +++ b/pod/recorder/plugins/type_audiovideocast.py @@ -99,6 +99,9 @@ def save_video(recording, video_data, video_src): video.is_360 = recorder.is_360 # Désactiver les commentaires video.disable_comment = recorder.disable_comment + # add sites + video.sites.add(*recorder.sites.all()) + video.save() encode_video = getattr(encode, ENCODE_VIDEO) encode_video(video.id) diff --git a/pod/recorder/plugins/type_video.py b/pod/recorder/plugins/type_video.py index 7ccf30572a..b87103046e 100644 --- a/pod/recorder/plugins/type_video.py +++ b/pod/recorder/plugins/type_video.py @@ -76,6 +76,8 @@ def encode_recording(recording): video.is_360 = recorder.is_360 # Désactiver les commentaires video.disable_comment = recorder.disable_comment + # add sites + video.sites.add(*recorder.sites.all()) video.save() encode_video = getattr(encode, ENCODE_VIDEO) diff --git a/pod/settings.py b/pod/settings.py index e0dfda0281..f0fe8d07df 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -9,7 +9,7 @@ ## # Version of the project # -VERSION = '2.8' +VERSION = '2.8.1' ## # Installed applications list diff --git a/pod/video/encode.py b/pod/video/encode.py index 4cb066d662..d2f2ea1901 100755 --- a/pod/video/encode.py +++ b/pod/video/encode.py @@ -14,6 +14,8 @@ from .utils import change_encoding_step, add_encoding_log, check_file from .utils import create_outputdir, send_email, send_email_encoding +from .utils import get_duration_from_mp4 +from .utils import fix_video_duration # from pod.main.context_processors import TEMPLATE_VISIBLE_SETTINGS from pod.main.tasks import task_start_encode @@ -331,14 +333,17 @@ def encode_video(video_id): video_id, 4, "encoding video file: 7/11 remove_previous_overview") remove_previous_overview(overviewfilename, overviewimagefilename) + video_duration = video_data["duration"] if ( + video_data["duration"] > 0) else get_duration_from_mp4( + video_mp4.source_file.path, output_dir) nb_img = 99 if ( - video_data["duration"] > 99) else video_data["duration"] + video_duration > 99) else video_duration change_encoding_step( video_id, 4, "encoding video file: 8/11 create_overview_image") msg = create_overview_image( video_id, - video_mp4.video.video.path, video_data["duration"], + video_mp4.source_file.path, video_duration, nb_img, image_width, overviewimagefilename, overviewfilename) add_encoding_log( video_id, @@ -348,7 +353,7 @@ def encode_video(video_id): video_id, 4, "encoding video file: 11/11 create_and_save_thumbnails") msg = create_and_save_thumbnails( - video_mp4.video.video.path, video_mp4.width, video_id) + video_mp4.source_file.path, video_mp4.width, video_id) add_encoding_log( video_id, "create_and_save_thumbnails: %s" % msg) @@ -369,6 +374,8 @@ def encode_video(video_id): video_to_encode.encoding_in_progress = False video_to_encode.save() + fix_video_duration(video_id, output_dir) + # End add_encoding_log(video_id, "End: %s" % time.ctime()) with open(output_dir + "/encoding.log", "a") as f: @@ -429,7 +436,12 @@ def get_video_data(video_id, output_dir): duration = 0 key_frames_interval = 0 if len(info["streams"]) > 0: - is_video = True + codec = info["streams"][0].get("codec_name", "unknown") + image_codec = ["jpeg", "gif", "png", "bmp", "jpg"] + is_stream_thumbnail = any(ext in codec.lower() + for ext in image_codec) + if not is_stream_thumbnail: + is_video = True if info["streams"][0].get('height'): in_height = info["streams"][0]['height'] """ diff --git a/pod/video/encode_gpu.py b/pod/video/encode_gpu.py index 5fbdb819b8..38126cb23f 100755 --- a/pod/video/encode_gpu.py +++ b/pod/video/encode_gpu.py @@ -120,7 +120,7 @@ def encode_with_gpu(format, codec, height, file): msg += "Encode CPU %s ok \n" % format return_value = True if return_value is False: - msg += 20 * "////" + "\n" + msg += 20*"////" + "\n" msg += 'ERROR ENCODING %s FOR FILE %s \n' % (format, file) encode_log(msg) return return_value @@ -510,8 +510,6 @@ def get_info_video(file): msg = "--> get_info_video" + "\n" probe_cmd = 'ffprobe -v quiet -show_format -show_streams \ -print_format json -i {}/{}'.format(VIDEOS_DIR, file) - if DEBUG: - print(probe_cmd) msg += probe_cmd + "\n" has_stream_video = False has_stream_thumbnail = False @@ -523,7 +521,10 @@ def get_info_video(file): msg += json.dumps(info, indent=2) msg += " \n" msg += return_msg + "\n" - duration = int(float("%s" % info["format"]['duration'])) + try: + duration = int(float("%s" % info["format"]['duration'])) + except (RuntimeError, KeyError, AttributeError, ValueError) as err: + msg += "\nUnexpected error: {0}".format(err) streams = info.get("streams", []) for stream in streams: msg += stream.get("codec_type", "unknown") @@ -538,9 +539,7 @@ def get_info_video(file): height = stream.get("height", 0) if stream.get("codec_type", "unknown") == "audio": has_stream_audio = True - encode_log(msg) - return { "has_stream_video": has_stream_video, "has_stream_thumbnail": has_stream_thumbnail, @@ -706,6 +705,8 @@ def add_info_video(key, value, append=False): info_video = {} info_video = get_info_video(filename) + if not DEBUG and info_video["duration"] > 0: + SUBTIME = ' -ss 0 -to %s ' % info_video["duration"] msg += " \n" msg += json.dumps(info_video, indent=2) msg += " \n" diff --git a/pod/video/forms.py b/pod/video/forms.py index 81caedf56c..9de7805e12 100644 --- a/pod/video/forms.py +++ b/pod/video/forms.py @@ -542,7 +542,7 @@ def set_queryset(self): user_channels = Channel.objects.all() if self.is_superuser else ( self.current_user.owners_channels.all( ) | self.current_user.users_channels.all( - ) | Channel.objects.filter(allow_to_groups=users_groups) + ) | Channel.objects.filter(allow_to_groups__in=users_groups) ).distinct() user_channels.filter(sites=get_current_site(None)) if user_channels: diff --git a/pod/video/remote_encode.py b/pod/video/remote_encode.py index 2a054d0422..0e7636ace0 100644 --- a/pod/video/remote_encode.py +++ b/pod/video/remote_encode.py @@ -22,6 +22,7 @@ from .utils import change_encoding_step, add_encoding_log, check_file from .utils import create_outputdir, send_email, send_email_encoding +from .utils import fix_video_duration, get_duration_from_mp4 if getattr(settings, 'USE_PODFILE', False): FILEPICKER = True @@ -54,12 +55,21 @@ settings, 'SSH_REMOTE_KEY', "") SSH_REMOTE_CMD = getattr( settings, 'SSH_REMOTE_CMD', "") - +ENCODING_CHOICES = getattr( + settings, 'ENCODING_CHOICES', ( + ("audio", "audio"), + ("360p", "360p"), + ("480p", "480p"), + ("720p", "720p"), + ("1080p", "1080p"), + ("playlist", "playlist") + )) # ########################################################################## # REMOTE ENCODE VIDEO : THREAD TO LAUNCH ENCODE # ########################################################################## + def start_store_remote_encoding_video(video_id): log.info("START STORE ENCODED FILES FOR VIDEO ID %s" % video_id) t = threading.Thread(target=store_remote_encoding_video, @@ -195,6 +205,8 @@ def store_remote_encoding_video(video_id): video_encoding.encoding_in_progress = False video_encoding.save() + fix_video_duration(video_encoding.id, output_dir) + # End add_encoding_log(video_id, "End : %s" % time.ctime()) with open(output_dir + "/encoding.log", "a") as f: @@ -271,20 +283,23 @@ def remote_video_part(video_to_encode, info_video, output_dir): image_width = video_mp4.width / 4 # width of generate image file change_encoding_step( video_id, 4, - "encoding video file : 7/11 remove_previous_overview") + "encoding video file: 7/11 remove_previous_overview") remove_previous_overview(overviewfilename, overviewimagefilename) + video_duration = info_video["duration"] if ( + info_video["duration"] > 0) else get_duration_from_mp4( + video_mp4.source_file.path, output_dir) nb_img = 99 if ( - info_video["duration"] > 99) else info_video["duration"] + video_duration > 99) else video_duration change_encoding_step( video_id, 4, - "encoding video file : 8/11 create_overview_image") + "encoding video file: 8/11 create_overview_image") msg_overview = create_overview_image( video_id, - video_mp4.video.video.path, info_video["duration"], + video_mp4.source_file.path, video_duration, nb_img, image_width, overviewimagefilename, overviewfilename) add_encoding_log( video_id, - "create_overview_image : %s" % msg_overview) + "create_overview_image: %s" % msg_overview) # create thumbnail if ( info_video["has_stream_thumbnail"] @@ -463,7 +478,7 @@ def import_mp4(encod_video, output_dir, video_to_encode): rendition = VideoRendition.objects.get( resolution=encod_video["rendition"]) encoding, created = EncodingVideo.objects.get_or_create( - name=filename, + name=get_encoding_choice_from_filename(filename), video=video_to_encode, rendition=rendition, encoding_format="video/mp4") @@ -503,7 +518,7 @@ def import_m3u8(encod_video, output_dir, video_to_encode): ): encoding, created = EncodingVideo.objects.get_or_create( - name=filename, + name=get_encoding_choice_from_filename(filename), video=video_to_encode, rendition=rendition, encoding_format="video/mp2t") @@ -512,7 +527,7 @@ def import_m3u8(encod_video, output_dir, video_to_encode): encoding.save() playlist, created = PlaylistVideo.objects.get_or_create( - name=filename, + name=get_encoding_choice_from_filename(filename), video=video_to_encode, encoding_format="application/x-mpegURL") playlist.source_file = videofilenameM3u8.replace( @@ -532,3 +547,10 @@ def import_m3u8(encod_video, output_dir, video_to_encode): send_email(msg, video_to_encode.id) return msg, master_playlist + + +def get_encoding_choice_from_filename(filename): + choices = {} + for choice in ENCODING_CHOICES: + choices[choice[0][:3]] = choice[0] + return choices.get(filename[:3], "360p") diff --git a/pod/video/templates/videos/video_record.html b/pod/video/templates/videos/video_record.html index ad4ba4c0e0..dfc8c3a253 100644 --- a/pod/video/templates/videos/video_record.html +++ b/pod/video/templates/videos/video_record.html @@ -100,7 +100,10 @@ {% block more_script %} {% endblock more_script %} diff --git a/pod/video/tests/test_views.py b/pod/video/tests/test_views.py index a6c7cc8b30..d04fae66cf 100644 --- a/pod/video/tests/test_views.py +++ b/pod/video/tests/test_views.py @@ -1,8 +1,10 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client from django.contrib.auth.models import User from pod.authentication.models import AccessGroup +from django.core.urlresolvers import reverse +from django.contrib.sites.models import Site from ..models import Channel from ..models import Theme @@ -10,8 +12,11 @@ from ..models import Type from ..models import Discipline from ..models import AdvancedNotes -from django.contrib.sites.models import Site +from .. import views + +from importlib import reload import re +import json class ChannelTestView(TestCase): @@ -881,6 +886,56 @@ def test_video_recordTestView_get_request(self): self.client.force_login(self.user) response = self.client.get("/video_record/") self.assertEqual(response.status_code, 200) + print( " ---> test_video_recordTestView_get_request" " of video_recordTestView : OK !") + + @override_settings( + DEBUG=True, + RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY=True + ) + def test_video_recordTestView_get_request_restrict(self): + reload(views) + self.client = Client() + response = self.client.get("/video_record/") + self.assertEqual(response.status_code, 302) + self.user = User.objects.get(username="pod") + self.client.force_login(self.user) + response = self.client.get("/video_record/") + self.assertEquals(response.context['access_not_allowed'], True) + self.user.is_staff = True + self.user.save() + response = self.client.get("/video_record/") + self.assertEqual(response.status_code, 200) + print( + " ---> test_video_recordTestView_get_request_restrict" + " of video_recordTestView : OK !") + + def test_video_recordTestView_upload_recordvideo(self): + reload(views) + video = SimpleUploadedFile( + "file.mp4", b"file_content", content_type="video/mp4") + self.client = Client() + self.user = User.objects.get(username="pod") + self.client.force_login(self.user) + response = self.client.post( + reverse('video_record'), + {'video': video, 'title': 'test upload'}, + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context, None) + try: + data = json.loads(response.content) + except (RuntimeError, TypeError, NameError, AttributeError) as err: + print("Unexpected error: {0}".format(err)) + data = json.loads(response.content.decode("utf-8")) + self.assertEqual(data['id'], 1) + self.assertEqual(data['url_edit'], "/video_edit/0001-test-upload/") + self.assertEqual(Video.objects.all().count(), 1) + vid = Video.objects.get(id=1) + self.assertEqual(vid.title, 'test upload') + print( + " ---> test_video_recordTestView_upload_recordvideo" + " of video_recordTestView : OK !") diff --git a/pod/video/utils.py b/pod/video/utils.py index b013e824e7..865208775d 100644 --- a/pod/video/utils.py +++ b/pod/video/utils.py @@ -4,12 +4,14 @@ from django.core.mail import mail_admins from django.core.mail import mail_managers from django.core.mail import EmailMultiAlternatives +from django.core.exceptions import ObjectDoesNotExist from .models import EncodingStep from .models import EncodingLog -from .models import Video +from .models import Video, EncodingAudio, EncodingVideo import os +import subprocess DEBUG = getattr(settings, 'DEBUG', True) @@ -44,12 +46,72 @@ SECURE_SSL_REDIRECT = getattr(settings, 'SECURE_SSL_REDIRECT', False) +FFPROBE = getattr(settings, 'FFPROBE', 'ffprobe') + +GET_DURATION_VIDEO = getattr( + settings, + 'GET_DURATION_VIDEO', + "%(ffprobe)s -v error -show_entries format=duration " + + "-of default=noprint_wrappers=1:nokey=1 -i %(source)s") # ########################################################################## # ENCODE VIDEO : GENERIC FUNCTION # ########################################################################## +def get_duration_from_mp4(mp4_file, output_dir): + msg = "" + duration = 0 + command = GET_DURATION_VIDEO % {'ffprobe': FFPROBE, 'source': mp4_file} + ffproberesult = subprocess.run( + command, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + msg += "\nduration ffprobe command: \n- %s\n" % command + + try: + duration = int(float("%s" % ffproberesult.stdout.decode('utf-8'))) + except (RuntimeError, TypeError, AttributeError, ValueError) as err: + msg += "\nUnexpected error: {0}".format(err) + + with open(output_dir + "/encoding.log", "a") as f: + f.write(msg) + f.write('\nffprobe command duration video result: ') + f.write(ffproberesult.stdout.decode('utf-8')+"\n") + f.write("Duration : %s \n\n" % duration) + + if DEBUG: + print(msg) + print('\nffprobe command duration video result: ') + print(ffproberesult.stdout.decode('utf-8')) + print("Duration : %s" % duration) + + return duration + + +def fix_video_duration(video_id, output_dir): + try: + vid = Video.objects.get(id=video_id) + except ObjectDoesNotExist as err: + print("ObjectDoesNotExist error: {0}".format(err)) + return + if vid.duration == 0: + if vid.is_video: + ev = EncodingVideo.objects.filter( + video=vid, encoding_format="video/mp4") + if ev.count() > 0: + video_mp4 = sorted(ev, key=lambda m: m.height)[0] + vid.duration = get_duration_from_mp4( + video_mp4.source_file.path, output_dir) + vid.save() + else: + ea = EncodingAudio.objects.filter( + video=vid, encoding_format="audio/mp3") + if ea.count() > 0: + vid.duration = get_duration_from_mp4( + ea.first().source_file.path, output_dir) + vid.save() + + def change_encoding_step(video_id, num_step, desc): """Change encoding step.""" encoding_step, created = EncodingStep.objects.get_or_create( diff --git a/pod/video/views.py b/pod/video/views.py index 459a22edea..3c6df06d1a 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -42,6 +42,7 @@ from pod.video.forms import VideoPasswordForm from pod.video.forms import VideoDeleteForm from pod.video.forms import AdvancedNotesForm, NoteCommentsForm + from django.views.decorators.csrf import ensure_csrf_cookie from django.core.exceptions import ObjectDoesNotExist import json @@ -141,6 +142,11 @@ ACTIVE_VIDEO_COMMENT = getattr(settings, 'ACTIVE_VIDEO_COMMENT', False) USE_CATEGORY = getattr(settings, 'USER_VIDEO_CATEGORY', False) +DEFAULT_RECORDER_TYPE_ID = getattr( + settings, 'DEFAULT_RECORDER_TYPE_ID', + 1 +) + # ############################################################################ # CHANNEL # ############################################################################ @@ -735,51 +741,29 @@ def video_edit(request, slug=None): current_lang=request.LANGUAGE_CODE, ) - return check_form(request, form) - + if form.is_valid(): + video = save_video_form(request, form) + messages.add_message( + request, messages.INFO, + _('The changes have been saved.') + ) + if request.POST.get('_saveandsee'): + return redirect( + reverse('video', args=(video.slug,)) + ) + else: + return redirect( + reverse('video_edit', args=(video.slug,)) + ) + else: + messages.add_message( + request, messages.ERROR, + _(u'One or more errors have been found in the form.')) return render(request, 'videos/video_edit.html', { 'form': form} ) -def check_form(request, form): - if form.is_valid(): - video = save_video_form(request, form) - messages.add_message( - request, messages.INFO, - _('The changes have been saved.') - ) - if request.is_ajax(): - return JsonResponse({ - "id": video.id, - "url_edit": reverse('video_edit', args=(video.slug,)), - }) - - if request.POST.get('_saveandsee'): - return redirect( - reverse('video', args=(video.slug,)) - ) - else: - return redirect( - reverse('video_edit', args=(video.slug,)) - ) - else: - if request.is_ajax(): - return JsonResponse( - { - "success": False, - "error": - _(u'One or more errors have been found in the form.') - } - ) - messages.add_message( - request, messages.ERROR, - _(u'One or more errors have been found in the form.')) - return render(request, 'videos/video_edit.html', { - 'form': form} - ) - - def save_video_form(request, form): video = form.save(commit=False) if ( @@ -2263,6 +2247,31 @@ def get_response_data(self, chunked_upload, request): @csrf_protect @login_required(redirect_field_name='referrer') def video_record(request): + if (RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY + and request.user.is_staff is False): + return render(request, + 'videos/video_edit.html', + {'access_not_allowed': True} + ) + if request.method == 'POST' and request.is_ajax(): + try: + vid = Video() + vid.video = request.FILES['video'] + vid.title = request.POST['title'] + vid.owner = request.user + vid.type = Type.objects.get(id=DEFAULT_RECORDER_TYPE_ID) + vid.save() + vid.sites.add(get_current_site(request)) + vid.launch_encode = True + vid.save() + return JsonResponse({ + "id": vid.id, + "url_edit": reverse('video_edit', args=(vid.slug,)), + }) + except (RuntimeError, TypeError, NameError, AttributeError) as err: + return JsonResponse({ + "error": "Unexpected error: {0}".format(err), + }) return render(request, 'videos/video_record.html', {})