-
video src의 썸네일을 추출할 때 canvas의 CORS 이슈(SecurityError) 우회하는 방법기타 2022. 4. 20. 21:13
(코드는 맨 밑에 있으니 코드만 확인하실 분은 맨 밑으로 내리시면 됩니다)
React 웹에서 고객이 업로드할 영상을 골랐을 때, 썸네일 파일을 자동으로 생성하는 코드를 작성할 일이 있었다.
원래 React Native에선 라이브러리를 통해 뚝딱 해결했던 일이라 마찬가지로 React 라이브러리를 찾아보려했는데 생각외로 쉽지 않았다.
일단 구글링으로 알아낸 로직은 다음과 같았다.
이미지 파일 추출 로직
- 특정한 <img>가 존재한다.
- <canvas> 에 그 <img>의 src를 그린다.
- canvas.toDataURL() 을 통해 이미지 데이터를 얻는다.
- 얻은 이미지 데이터로 파일을 생성한다.
첫번째 문제 : 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 }); };
시행착오를 많이 했는데 관련 내용을 찾기 어려워서 한번 정리를 해봤다.
우회에 성공한 방법을 기록했을 뿐 이게 좋은 방법이란 의미는 아닙니다. 이런식으로 우회를 해도 되는지에 대해 잘 모르겠네요. 아직 제가 많이 부족해서 그러니 문제 있으면 말씀 부탁 드립니다 :)
'기타' 카테고리의 다른 글
[CSS] position 과 overflow를 활용한 웹 위의 모바일 화면 만들기 (0) 2022.07.24 깃허브 페이지와 한글 도메인 연결하기 (feat.내도메인.한국) (1) 2021.12.19