ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • video src의 썸네일을 추출할 때 canvas의 CORS 이슈(SecurityError) 우회하는 방법
    기타 2022. 4. 20. 21:13

    (코드는 맨 밑에 있으니 코드만 확인하실 분은 맨 밑으로 내리시면 됩니다)

     

    React 웹에서 고객이 업로드할 영상을 골랐을 때, 썸네일 파일을 자동으로 생성하는 코드를 작성할 일이 있었다.

    원래 React Native에선 라이브러리를 통해 뚝딱 해결했던 일이라 마찬가지로 React 라이브러리를 찾아보려했는데 생각외로 쉽지 않았다.

     

    일단 구글링으로 알아낸 로직은 다음과 같았다.

     

    이미지 파일 추출 로직

    1. 특정한 <img>가 존재한다.
    2. <canvas> 에 그 <img>의 src를 그린다.
    3. canvas.toDataURL() 을 통해 이미지 데이터를 얻는다.
    4. 얻은 이미지 데이터로 파일을 생성한다.

     

    첫번째 문제 : SecurityError

    방법을 알고 신나게 시도를 해보았으나..

    아래와 같이 canvas.toDataURL() 부분에서 SecurityError 가 발생했다.

    209번째 줄에 toDataURL을 시도한 부분에서 문제가 생겼다

    처음에는 이게 무슨일인가 당황했다. 찾다보니 이게 바로 그 초보개발자들을 괴롭힌다는(?) CORS 관련 이슈였다.

    출처가 다른 경우, <canvas> 에 이미지를 가져다 그리는건 자유지만 그 순간부터 <canvas>가 오염된(tainted) 것으로 판단되기 때문에 관련 데이터를 뺄 수 없게 된 것이다. 

     

    canvas가 오염된 경우, 아래 세 가지를 시도할 때 SecurityError 가 발생한다.

    • Calling getImageData() on the canvas's context
    • Calling toBlob() on the <canvas> element itself
    • Calling toDataURL() on the canvas

     

    그래서 에러를 우회할 수 있는 방법을 찾아 헤맸고, 아래 공식문서를 통해 해결법을 찾을 수 있었다.

    https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image

     

    Allowing cross-origin use of images and canvas - HTML: HyperText Markup Language | MDN

    HTML provides a crossorigin attribute for images that, in combination with an appropriate CORS header, allows images defined by the <img> element that are loaded from foreign origins to be used in a <canvas> as if they had been loaded from the current orig

    developer.mozilla.org

    새로운 Image 객체를 만들어서 crossOrigin 값에 anonymous를 넣어주는 것이 핵심이다.

     

     

    두 번째 문제 : 사파리와 크롬의 img 처리 방식 차이

    iOS에서 테스트 할때는 잘 돌아가서 이제 끝났다고 안심한 찰나, 안드로이드에서 테스트하자 썸네일이 생성되지 않았다.

    사파리에서는 Image 객체에 영상소스를 넣어도 이미지를 만들어주었지만, 크롬에서는 Image 객체에 영상 소스를 넣을 경우 아예 이미지 로드가 되지 않았다. 

     

    알고보니 Image에 영상을 넣어도 작동하는건 사파리만의 기능이었다.

    https://stackoverflow.com/questions/61852787/chrome-not-showing-videos

     

    Chrome not showing videos

    For some reason the videos are being displayed in Safari but not when I open it in Chrome? I don't know if I've set something wrong or if it's something else? I'm attaching a screenshot of what it ...

    stackoverflow.com

    결국 <img> 가 아니라 <video> 를 통해 문제를 해결해야 했다.

     

    코드

    돌고돌아서 결국 성공한 코드는 아래와 같다.

    // 컴포넌트 내 미리 비디오 만들어서 선언
    let tempThumbnailVideo = document.createElement('video');
    
    // videoUrl은 고객이 선택한 영상의 url
    const setVideoUrl = (videoUrl) => {
      tempThumbnailVideo.crossOrigin = 'Anonymous';
    
      // 영상이 로드된 이후 썸네일 생성하도록 이벤트리스너 설정 
      tempThumbnailVideo.addEventListener(
        'loadeddata',
    	() => {
    	  generateThumbnail(videoUrl);
    	},
        false
      );
    	
      // 첫 시작에는 검은색 빈 화면일 수 있어서 t=0.1로 설정
      tempThumbnailVideo.src = `${videoUrl}#t=0.1`;
      }
    }
    
    const generateThumbnail = (videoUrl) => {
      let canvas = document.createElement('canvas');
      let context = canvas.getContext('2d');
    
      // 썸네일 크기 설정
      canvas.width = tempThumbnailVideo.videoWidth * 0.25;
      canvas.height = tempThumbnailVideo.videoHeight * 0.25;
    
      context?.drawImage(tempThumbnailVideo, 0, 0, canvas.width, canvas.height);
    
      const dataURI = canvas.toDataURL('image/jpeg');
      const file = dataURLtoFile(dataURI, `${+new Date()}.jpeg`);
      
      // 이후 필요에 따라 파일을 서버에 전송하는 로직 작성
    };

     

    파일을 생성하는 함수는 다른 util 파일에 저장해두고 import 해서 사용했다.

    이 코드는 그냥 구글링해서 나온 내용을 그대로 복붙했다.

    export const dataURLtoFile = (dataurl, filename) => {
      const arr = dataurl.split(',');
      const mime = arr[0].match(/:(.*?);/)![1];
      const bstr = atob(arr[1]);
      let n = bstr.length;
      const u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, { type: mime });
    };

     

    시행착오를 많이 했는데 관련 내용을 찾기 어려워서 한번 정리를 해봤다.

     


     

    우회에 성공한 방법을 기록했을 뿐 이게 좋은 방법이란 의미는 아닙니다. 이런식으로 우회를 해도 되는지에 대해 잘 모르겠네요. 아직 제가 많이 부족해서 그러니 문제 있으면 말씀 부탁 드립니다 :)

     

    댓글

Designed by Tistory.