반응형

목표

Lambda Function에 python을 통해

영상편집(영상 + 이미지 + 텍스트 이미지화)을 하는 Function을 만드는게 목표.


환경

  1. python 3.7
  2. moviepy (ffmpeg + imageMagick)
  3. Lambda + Api Gateway + CloudWatch + S3

프로세스

  1. API Gateway를 통한 동영상 합성 요청
  2. Lambda 에서 Trigger를 통한 동영상 합성 진행
  3. S3로 바로 결과 파일 생성

2. Lambda 에서 Trigger를 통한 동영상 합성 진행항목을 중점적으로 다뤄본다.

해당 내용을 진행하기 전에 Lambda에 대해서 알아두어야할 점이 있다.

  1. OS( Amazone Linux2 )

  2. 서버의 경로가 존재한다.

  3. Lambda는 소스파일을 50M 업로드 제한이 있다.

    1. Lambda 기본 Storage를 활용한다. ( /tmp ) 20GB제한

      • tmp는 영상 임시 저장 경로로 활용하였다.
        추천하지 않는 방법 내 생각엔 tmp에 소스를 옮기는 작업은 비효율적이다.

        기본적으로 부착되는 Storage이기 때문에 영상을 받고 S3로 업로드하기 위한 임시 경로

    2. Lambda Layer를 활용한다( /opt )

      • 해당 내용은 소스코드를 참조하여 진행하면된다.

        https://github.com/serverlesspub/imagemagick-aws-lambda-2
        해당 코드는 Layer에 ImageMagick이라는 Utilitiy를 배포할수있는 스크립트이다.


        내 버전에서는 freetype 을 지원하여하는데 해당버전을 지원하게 하려면 수정이 필요하였다.

        https://github.com/serverlesspub/imagemagick-aws-lambda-2/blob/master/Makefile_ImageMagick
        위 파일의 내용을 수정해야하는데

        https://github.com/serverlesspub/imagemagick-aws-lambda-2/issues/13
        위 이슈를 참고하여 수정하였다.

          LIBPNG_VERSION ?= 1.6.37
          LIBJPG_VERSION ?= 9c
          OPENJP2_VERSION ?= 2.3.1
          LIBTIFF_VERSION ?= 4.0.9
          BZIP2_VERSION ?= 1.0.6
          LIBWEBP_VERSION ?= 0.6.1
          IMAGEMAGICK_VERSION ?= 7.0.8-45
          **FREETYPE_VERSION ?= 2.8.1**
        
          TARGET_DIR ?= /opt/
          PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
          CACHE_DIR=$(PROJECT_ROOT)build/cache
        
          .ONESHELL:
        
          CONFIGURE = PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
              ./configure \
                  CPPFLAGS=-I$(CACHE_DIR)/include \
                  LDFLAGS=-L$(CACHE_DIR)/lib \
                  --disable-dependency-tracking \
                  --disable-shared \
                  --enable-static \
                  --prefix=$(CACHE_DIR)
        
          **##freetype
        
          FREETYPE_SOURCE=freetype-$(FREETYPE_VERSION).tar.gz
        
          $(FREETYPE_SOURCE):
              curl -LO http://www.imagemagick.org/download/delegates//$(FREETYPE_SOURCE)
        
          $(CACHE_DIR)/lib/freetype.a: $(FREETYPE_SOURCE)
              tar xf $<
              cd freetype-*
              $(CONFIGURE)
              make clean
              make install**
        
          ## libjpg
        
          LIBJPG_SOURCE=jpegsrc.v$(LIBJPG_VERSION).tar.gz
        
          $(LIBJPG_SOURCE):
              curl -LO http://ijg.org/files/$(LIBJPG_SOURCE)
        
          $(CACHE_DIR)/lib/libjpeg.a: $(LIBJPG_SOURCE)
              tar xf $<
              cd jpeg*
              $(CONFIGURE)     
              make
              make install
        
          ## libpng
        
          LIBPNG_SOURCE=libpng-$(LIBPNG_VERSION).tar.xz
        
          $(LIBPNG_SOURCE):
              curl -LO http://prdownloads.sourceforge.net/libpng/$(LIBPNG_SOURCE)
        
          $(CACHE_DIR)/lib/libpng.a: $(LIBPNG_SOURCE)
              tar xf $<
              cd libpng*
              $(CONFIGURE)     
              make
              make install
        
          # libbz2
        
          BZIP2_SOURCE=bzip2-$(BZIP2_VERSION).tar.gz
        
          $(BZIP2_SOURCE):
              curl -LO http://prdownloads.sourceforge.net/bzip2/bzip2-$(BZIP2_VERSION).tar.gz
        
          $(CACHE_DIR)/lib/libbz2.a: $(BZIP2_SOURCE)
              tar xf $<
              cd bzip2-*
              make libbz2.a
              make install PREFIX=$(CACHE_DIR)
        
          # libtiff
        
          LIBTIFF_SOURCE=tiff-$(LIBTIFF_VERSION).tar.gz
        
          $(LIBTIFF_SOURCE):
              curl -LO http://download.osgeo.org/libtiff/$(LIBTIFF_SOURCE)
        
          $(CACHE_DIR)/lib/libtiff.a: $(LIBTIFF_SOURCE) $(CACHE_DIR)/lib/libjpeg.a
              tar xf $<
              cd tiff-*
              $(CONFIGURE)     
              make
              make install
        
          # libwebp
        
          LIBWEBP_SOURCE=libwebp-$(LIBWEBP_VERSION).tar.gz
        
          $(LIBWEBP_SOURCE):
              curl -L https://github.com/webmproject/libwebp/archive/v$(LIBWEBP_VERSION).tar.gz -o $(LIBWEBP_SOURCE)
        
          $(CACHE_DIR)/lib/libwebp.a: $(LIBWEBP_SOURCE)
              tar xf $<
              cd libwebp-*
              sh autogen.sh
              $(CONFIGURE)     
              make
              make install
        
          ## libopenjp2
        
          OPENJP2_SOURCE=openjp2-$(OPENJP2_VERSION).tar.gz
        
          $(OPENJP2_SOURCE):
              curl -L https://github.com/uclouvain/openjpeg/archive/v$(OPENJP2_VERSION).tar.gz -o $(OPENJP2_SOURCE)
        
          $(CACHE_DIR)/lib/libopenjp2.a: $(OPENJP2_SOURCE) $(CACHE_DIR)/lib/libpng.a $(CACHE_DIR)/lib/libtiff.a
              tar xf $<
              cd openjpeg-*
              mkdir -p build
              cd build 
              PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig cmake .. \
                  -DCMAKE_BUILD_TYPE=Release \
                  -DCMAKE_INSTALL_PREFIX=$(CACHE_DIR) \
                  -DBUILD_SHARED_LIBS:bool=off \
                  -DBUILD_CODEC:bool=off
              make clean
              make install
        
          ## ImageMagick
        
          IMAGE_MAGICK_SOURCE=ImageMagick-$(IMAGEMAGICK_VERSION).tar.gz
        
          $(IMAGE_MAGICK_SOURCE):
              curl -L https://github.com/ImageMagick/ImageMagick/archive/$(IMAGEMAGICK_VERSION).tar.gz -o $(IMAGE_MAGICK_SOURCE)
        
          LIBS:=$(CACHE_DIR)/lib/libjpeg.a \
              $(CACHE_DIR)/lib/libpng.a \
              $(CACHE_DIR)/lib/libopenjp2.a \
              $(CACHE_DIR)/lib/libtiff.a \
              $(CACHE_DIR)/lib/libbz2.a \
              **$(CACHE_DIR)/lib/freetype.a \**
              $(CACHE_DIR)/lib/libwebp.a
        
          $(TARGET_DIR)/bin/identify: $(IMAGE_MAGICK_SOURCE) $(LIBS)
              tar xf $<
              cd ImageMa*
              PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
                  ./configure \
                  CPPFLAGS=-I$(CACHE_DIR)/include \
                  LDFLAGS=-L$(CACHE_DIR)/lib \
                  --disable-dependency-tracking \
                  --disable-shared \
                  --enable-static \
                  --prefix=$(TARGET_DIR) \
                  --enable-delegate-build \
                  --without-modules \
                  --disable-docs \
                  --without-magick-plus-plus \
                  --without-perl \
                  --without-x \
                  --disable-openmp
              make clean
              make all
              make install
        
          libs: $(LIBS)
        
          all: $(TARGET_DIR)/bin/identify

        수정을 하고 깃허브 README.md파일을 참고로 배포를 진행 하면 Layer로 배포가 된다.


Layer에 적용한 모습

 

 

위 사진처럼 Layer에 내가 만든파일을 적용하게되면

/opt 경로를 접근할수 있게된다.

위의 상태로 소스코드만 작업해주면 된다.

/tmp의 경로에는 image, video가 떨어질 경로

import json
import os
import stat
import shutil
import boto3
import datetime

lambda_tmp_dir = '/tmp'

image_path = "{0}/{1}".format(lambda_tmp_dir, "images")
video_path = "{0}/{1}".format(lambda_tmp_dir, "video")
video_name = "video.mp4"

video_bucket = "heesun-video"

s3 = boto3.client ('s3')
s3.download_file('heesun','font/malgun.ttf','/tmp/malgun.ttf')

from moviepy.editor import *
from moviepy.config import *
change_settings({"IMAGEMAGICK_BINARY": "/opt/bin/magick"})

print('Loading function')

def prepare_path(target):
  if os.path.exists(target):
    shutil.rmtree(target)

  os.mkdir(target)

def move_video(video_file, bucket, dest_key):
    video = open(video_file,"r")

    s3.upload_file(
        video_file,bucket,dest_key,
        ExtraArgs={'ACL':'public-read'}
    )
# functions
def funcHeight(startSec, t):
    return 50+((startSec+t)*30)

def makeVideoFileClip(path, hasMask):
    return (VideoFileClip(path, has_mask=hasMask))

def makeTextClip(msg, fpath, fcolor, fsize):
    return (TextClip(msg, font=fpath, color=fcolor, fontsize=fsize))

# 텍스트가 위에서 아래로 내려오는 애니메이션
def applyTextAnimation(tclip, startSec, endSec):
    txt_col = tclip.on_color(size=(200,30), color=(0,0,0), pos=(6,'center'), col_opacity=0.6)
    txt_mov = (txt_col.set_pos(lambda t: ('center', funcHeight(startSec, t))))
    return (txt_mov.set_start(startSec).set_end(endSec))

def makeImageClip(path, startSec, endSec):
    return (ImageClip(path).set_start(startSec).set_end(endSec))

def createVideo(tTxt, tColor, tSize, iPath, startSec, endSec):    
    # Create Image
    iclip = (makeImageClip(iPath,startSec,endSec))

    # Create Text
    txt = (makeTextClip(tTxt, "/tmp/malgun.ttf", tColor, tSize))
    tclip = (applyTextAnimation(txt,startSec,endSec))

    # txt + image
    return (CompositeVideoClip([iclip, tclip]))

def lambda_handler(event, context):

    tdatetime = datetime.datetime.now()
    prefix = tdatetime.strftime('%Y/%m/%d/')
    prepare_path(video_path)
    video_file = "{0}/{1}".format(video_path, video_name)

    arr = [("등장!!", "http://heesun.s3-ap-northeast-1.amazonaws.com/first.jpg", 2, 3), ("또또또!! 등장!!", "http://heesun.s3-ap-northeast-1.amazonaws.com/two.jpg", 6, 7)]

    clip = (makeVideoFileClip('https://heesun.s3-ap-northeast-1.amazonaws.com/background.mp4', True))

    arr_final = [clip]

    for (txt, img, startsec, endsec) in arr:
        arr_final.append(createVideo(txt, 'white', 24, img, startsec, endsec))

    final = CompositeVideoClip(arr_final)
    final.subclip(0,8).write_videofile(video_file,fps=24,codec='libx264',temp_audiofile="/tmp/test.mp4")
    final.close()

    ymd = prefix.split('/')
    video_key = "{0}/{1}/{2}.mp4".format(ymd[0], ymd[1],"".join(ymd))
    move_video(video_file, video_bucket, video_key)        

    os.system ('cd /tmp && ls -al')

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

소스는 미흡하지만 원하는대로 동작이 되었다.

위 코드 내용에서 필요한것들은

  1. S3 버킷
  2. 이미지 2가지 ( 위 버킷에 넣을 두가지 )

두가지 이미지에 텍스트가 합성되어 추출된다.

해결된 사항

  1. audio=True 할 시에 writing 되는 path가 /tmp 를 바라보지 않고 있어서 writing이 되지 않음. → 설정 필요

    해당 내용은 임시로 Audio file이 Writing할때 경로를 지정해주어야 하는데 해당 부분이 없어서 시스템경로에 Writing되면서 발생된다. temp_audiofile 라는 parameter를 추가해주면 된다.

     final.write_videofile(video_file,fps=24,codec='libx264',temp_audiofile="/tmp/test.mp4")

    코어(라이브러리 파일)쪽에 손을 대는 방법이다. 현재 둘다 적용이 되어있다.

     # moveipy.video.VideoClip.py 
     # 306 line
     audiofile = (tempDirPath + "/" + name + Clip._TEMP_FILES_PREFIX +
                                  "wvf_snd.%s" % audio_ext)

해결되지 않은 사항

  1. Lambda Function Call이 반복될 때 반복적인 다운로드, 프로세스를 제거해야 함.

  2. CloudWatch를 통해서 logging을 추가 해야 함.

  3. tmp 디렉토리 안에 동영상파일이 계속 남아있는지 로그 체크 해봐야함.

    ⇒ 남아있진 않음. 그러나 여러차례 호출하는 api로 작업을 진행 및 테스트 해봐야 함.

    ⇒ 남아있다면 만들고 업로드 후 지워주는 프로세스 추가


Lambda 리소스 성능

램은 어느 일정 수준 이후로는 더 이상 초가 증가 되지 않으나 넉넉한 용량일 때 생성속도가 빨라진다.

이미지, 동영상 용량등이 생성 속도에 영향을 준걸로 확인 어느정도 크기 이후에는 큰 영향이 없음


이전에 기록했던 영상 합성 Lambda 함수를 다시 정리해 보았다.

영상 관련된 부분을 작업하려면 ffmpeg가 필요하였다.

되게 고생하면서 작업했던 기억이 있는데 글을 정리하려고보니 두서없이 정리된거 같다.

+ Recent posts