본문 바로가기
Experience/- KT AIVLE School

KT AIVLE School 빅프로젝트 - 웹캠 영상 녹화 시 에러 경험

by Yoojacha 2023. 7. 19.
        if dancer_video_file_extension != '.mp4':
            print('댄서 비디오 확장자: ', dancer_video_file_extension)
            subprocess.run(['ffmpeg', '-i', dancer_video_download_path, dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            dancer_video_download_path = dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')
        if danceable_video_file_extension != '.mp4':
            print('댄서브 비디오 확장자: ', danceable_video_file_extension)
            subprocess.run(['ffmpeg', '-i', danceable_video_download_path, danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            danceable_video_download_path = danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')

에러 발생 원인

react-webcam을 통해서 웹캠 스트리밍 영상을 녹화하여 webm 파일 생성하는 로직이 백엔드로 보내면서 에러가 발생했었습니다. 처음에는 단순히 백엔드에서 webm 파일을 인식하지 못해서 생기는 에러라고 생각했습니다. 그래서 프론트에서 webm 확장자 대신에 mp4로 바꿔도 동일한 에러가 발생했습니다. 

위의 에러를 보면 duration에 대한 단어가 등장했고, duration이 영어로는 재생 시간으로 이해를 하고 있어서 설마? 하는 마음으로 만들어진 영상을 카카오톡에 업로드해보니 분명 6초 짜리 정상 작동하는 영상임에도, 00:00으로 표시가 됐습니다. 

 

여기서 힌트를 얻어서 웹캠 영상을 녹화해서 백엔드에 업로드하는 영상의 meta 데이터가 없다보니 백엔드에서 moviePy를 통해 재생 시간 정보를 받아올 수 없어서 처리가 불가능하다보니 생긴 에러였습니다.

 

해결 방법

백엔드에서 처리했던 로직은 웹캠 녹화한 영상에 연습한 영상의 음악을 덮어주는 것이었습니다. 그래서 음악 자체의 길이를 추출해서 전달해주는 방법을 제가 제안해서 해결이 되었습니다. ffmpeg를 실행하기 위해서 subprocess를 사용했습니다.

        if dancer_video_file_extension != '.mp4':
            print('댄서 비디오 확장자: ', dancer_video_file_extension)
            subprocess.run(['ffmpeg', '-i', dancer_video_download_path, dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            dancer_video_download_path = dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')
        if danceable_video_file_extension != '.mp4':
            print('댄서블 비디오 확장자: ', danceable_video_file_extension)
            subprocess.run(['ffmpeg', '-i', danceable_video_download_path, danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            danceable_video_download_path = danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')

 

전체 코드

class EndPartDanceView(APIView):
    def post(self, request):
        try:
            user_info = get_user_info_from_token(request)
            user_id = user_info['userId']
            is_dancer = user_info['isDancer']
        except (TokenError, KeyError):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        if is_dancer:
            return Response(status=status.HTTP_403_FORBIDDEN)

        data = request.data

        if data['mosaic'] == 'true':
            is_mosaic = True
        else:
            is_mosaic = False

        section = VideoSection.objects.get(section_id=data['sectionId'])
        dancer_part_video = section.video
        info = '/'.join(dancer_part_video[:-5].split('/')[3:])

        dancer_video_file_extension = ''
        danceable_video_file_extension = '.' + data['video'].name.split('.')[-1]
        s3 = get_s3_client()

        # 버킷 내의 객체 목록 가져오기
        response = s3.list_objects_v2(Bucket='dancify-input')

        # 객체 목록에서 특정 문자열이 포함된 파일의 확장자를 추출
        for obj in response['Contents']:
            key = obj['Key']
            if info in key:
                dancer_video_file_extension = '.' + key.split('.')[-1]
                break

        # 댄서, 댄서블 영상 다운로드 폴더 경로 설정
        dancer_video_download_folder_path = os.path.join(settings.BASE_DIR,
                                                         'dancer_video', user_id)
        danceable_video_download_folder_path = os.path.join(settings.BASE_DIR,
                                                            'danceable_video', user_id)
        result_video_download_folder_path = os.path.join(settings.BASE_DIR,
                                                         'result_video', user_id)
        # 폴더 생성
        os.makedirs(dancer_video_download_folder_path, exist_ok=True)
        os.makedirs(danceable_video_download_folder_path, exist_ok=True)
        os.makedirs(result_video_download_folder_path, exist_ok=True)

        # 댄서, 댄서블 영상 다운로드 파일 경로 설정
        dancer_video_download_path = os.path.join(dancer_video_download_folder_path,
                                                  'video_dancer' + dancer_video_file_extension)
        danceable_video = request.FILES['video']
        danceable_video_download_path = os.path.join(danceable_video_download_folder_path,
                                                     'video_danceable' + '.' + danceable_video.name.split('.')[-1])
        result_video_download_path = os.path.join(result_video_download_folder_path, 'result.mp4')

        # 댄서, 댄서블 영상 다운로드(로컬에 저장)
        dancer_video_url = ORIGIN_VIDEO_DOMAIN + info + dancer_video_file_extension
        print(dancer_video_url[len(ORIGIN_VIDEO_DOMAIN):])
        s3.download_file('dancify-input', dancer_video_url[len(ORIGIN_VIDEO_DOMAIN):],
                         dancer_video_download_path)

        with open(danceable_video_download_path, 'wb') as destination:
            for chunk in danceable_video.chunks():
                destination.write(chunk)

        if dancer_video_file_extension != '.mp4':
            print('댄서 비디오 확장자: ', dancer_video_file_extension)
            subprocess.run(['ffmpeg', '-i', dancer_video_download_path, dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            dancer_video_download_path = dancer_video_download_path.replace(danceable_video_file_extension, '.mp4')
        if danceable_video_file_extension != '.mp4':
            print('댄서블 비디오 확장자: ', danceable_video_file_extension)
            subprocess.run(['ffmpeg', '-i', danceable_video_download_path, danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')])
            danceable_video_download_path = danceable_video_download_path.replace(danceable_video_file_extension, '.mp4')

        # 댄서의 오디오 추출, 댄서블의 비디오 추출
        audio = AudioFileClip(dancer_video_download_path)
        video = VideoFileClip(danceable_video_download_path)

        # 오디오, 비디오 파일 합치기
        result = video.set_audio(audio)
        result.write_videofile(result_video_download_path)

        with open(result_video_download_path, 'rb') as file:
            result_video = file.read()

        # 오디오 입힌 댄서블 비디오 업로드
        url_data = upload_video_with_metadata_to_s3(user_id, result_video,
                                                    'danceable', is_mosaic, '.mp4')
        shutil.rmtree(dancer_video_download_folder_path)
        shutil.rmtree(danceable_video_download_folder_path)
        shutil.rmtree(result_video_download_folder_path)

        data['video'] = url_data['video_url']
        data['thumbnail'] = url_data['thumbnail_url']
        data['keypoints'] = url_data['keypoint_url']

        # 로컬에 json 파일 저장
        # 폴더가 존재하지 않으면 생성
        path = os.path.join(settings.BASE_DIR, 'temp')
        if not os.path.exists(path):
            os.makedirs(path)

        # AWS 정보
        bucket_name = 'dancify-bucket'

        # 댄서 json 로컬에 저장
        dancer_json_path = download_json_from_s3(aws_bucket=bucket_name,
                                                 url=section.dancer_post.keypoints,
                                                 local_path=path)
        if dancer_json_path is None:
            return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        first_score_file_path = os.path.join(path, 'first.json')
        best_score_file_path = os.path.join(path, 'best.json')

        if 'firstScore' in request.FILES:
            file = request.FILES['firstScore']
            file_data = file.read()

            # 파일을 로컬에 저장
            with open(first_score_file_path, 'wb') as local_file:
                local_file.write(file_data)

        if 'bestScore' in request.FILES:
            file = request.FILES['bestScore']
            file_data = file.read()

            # 파일을 로컬에 저장
            with open(best_score_file_path, 'wb') as local_file:
                local_file.write(file_data)

        # AI 피드백 결과 반환
        first_score = json.dumps(create_json(str(dancer_json_path), str(first_score_file_path))).encode('utf-8')
        best_score = json.dumps(create_json(str(dancer_json_path), str(best_score_file_path))).encode('utf-8')

        folder_path = f'scores/{user_id}/'
        first_score_file_key = folder_path + \
            str(data['feedbackId']) + str(section.section_id) + '-first_score.json'
        best_score_file_key = folder_path + \
            str(data['feedbackId']) + str(section.section_id) + '-best_score.json'

        upload_obj_to_s3(bucket_name, folder_path,
                         first_score_file_key, first_score)
        upload_obj_to_s3(bucket_name, folder_path,
                         best_score_file_key, best_score)

        data['bestScore'] = AWS_DOMAIN + best_score_file_key
        data['firstScore'] = AWS_DOMAIN + first_score_file_key

        serializer = DanceableSectionSerializer(data=data)
        serializer.is_valid(raise_exception=True)
        serializer.save(feedback_post=FeedbackPost.objects.get(feedback_id=data['feedbackId']),
                        section=section)

        return Response(status=status.HTTP_200_OK)

 

 

댓글