react-native-webview html 소스를 활용한 VEVO youtube 영상 재생 실패 문의


(ddinggu) #1

안녕하세요.

제가 만들고 있는 앱은 react-native-webview로 유튜브 영상을 불러와 webview와 분리된 외부 react 컴포넌트에서 유튜브 영상을 일시정지 및 재생과 같은 컨트롤을 할 수 있는 앱을 만들고 있습니다. (iOS는 react-native-youtube를 사용하고 있습니다)

방식은 webview에 사용될 html text 파일을 하나 만든 후, message 주고 받을 수 있는 메소드를 추가하여 외부 컴포넌트와 소통할 수 있도록 만들어 놨습니다.

postMessage = (type, msg) => {
    const message = createMessage(type, msg);

    this.YTPlayer.postMessage(message);
};

onListenMessage = (event) => {
    const msg = event.nativeEvent.data;
    const parsingData = JSON.parse(msg);
    const { type, message } = parsingData;

    switch (type) {
      case 'CURRENTTIME':
        this.props.onProgress(message);
        break;

      case 'PLAYING':
        this.setState({ state: 'PLAYING' }, this.props.onChangeContentsPlay());
        break;

      case 'PAUSED':
        this.setState({ state: 'PAUSED' }, this.props.onChangeContentsPause());
        break;

      case 'UNSTARTED':
        this.setState({ state: 'UNSTARTED' }, this.props.onChangeContentsPause());
        break;

      case 'ERROR':
        this.setState({ state: 'ERROR' }, this.props.onError(message));
        break;

      case 'ENDED':
        this.setState({ state: 'ENDED' }, this.seekTo(this.props.startTime));
        break;

      default:
        break;
    }
  }

<WebView
    ref={this.onRef}
    originWhitelist={['*']}
    javaScriptEnabled
    domStorageEnabled
    mixedContentMode="always"
    onMessage={this.onListenMessage}
    source={YTsource(YTid, YOUTUBE_HEIGHT)}
/>

const YTsource = (videoId, height) => ({
  html: `<html>
<style type="text/css">    
    html, body {
        margin: 0;
        padding: 0;
        position: relative;
    }
</style>

<head>
    <meta name="viewport" content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, width=device-width">
</head>

<body>
    <div id="YTPlayer"></div>
</body>

<script type="text/javascript">
    var tag = document.createElement('script');
    tag.src = "https://www.youtube.com/iframe_api";
    var firstScriptTag = document.getElementsByTagName('script')[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

    var player;
    var timerId = 0;
    
    function onYouTubeIframeAPIReady() {
        player = new YT.Player('YTPlayer', {
            videoId: '${videoId}',
            height: ${height},                                                                                                                                                                                             
            width: '100%',
            events: {
                'onReady': onPlayerReady,
                'onStateChange': onPlayerStateChange,
                'onError': onPlayerError,
            },
            playerVars: {
                rel: 0,
                disablekb: 1,
               // origin: 'https://www.youtube.com'  not work!!!
            }
        });
    }

    function onPlayerReady(event) {
        player.unloadModule("captions");
        player.setOption("captions", "track", {"languageCode": "es"});
        event.target.setVolume(100);
    }

    function onPlayerStateChange(event) {
        clearTimeout(timerId);

        switch (event.data) {
            case YT.PlayerState.UNSTARTED:
                onStateChange("UNSTARTED");
                return;
                break;

            case YT.PlayerState.ENDED:
                onStateChange("ENDED");
                break;

            case YT.PlayerState.PLAYING:
                onStateChange("PLAYING");
                timerId = setInterval(function () {
                    setCurrentSeconds();
                }, 100);
                return;
                break;

            case YT.PlayerState.PAUSED:
                onStateChange("PAUSED");
                return;
                break;

            case YT.PlayerState.BUFFERING:
                onStateChange("BUFFERING");
                return;
                break;

            case YT.PlayerState.CUED:
                onStateChange("CUED");
                return;
                break;
        }
    }

    function onPlayerError(err) {
        var error = JSON.stringify(err);
        var msg = createWebViewMessage('ERROR', error);

        window.ReactNativeWebView.postMessage(msg);
    }

    window.document.addEventListener('message', function(e) {
        setFunction(e.data);
    });

    window.chopchopPlayerBridge = {
        sendStateChange: function (state) {
            var msg = createWebViewMessage(state, state.toLowerCase())
            window.ReactNativeWebView.postMessage(msg);
        }
    }
    
    function createWebViewMessage(type, msg) {
        var message = { type: type, message: msg };
        return JSON.stringify(message);
    }

    function setFunction(msg) {
        const { type, message } = JSON.parse(msg);

        switch (type) {
            case 'PLAY':
                playVideo();
                break;

            case 'PAUSE':
                pauseVideo();
                break;

            case 'SEEKTO':
                seekTo(message);                
                break;

            default:
                break;
        }
    }

    function pauseVideo() {
        player.pauseVideo();
    }

    function playVideo() {
        player.playVideo();
    }

    function seekTo(startSeconds) {
        player.seekTo(startSeconds, true);
    }
    

</script>

</html>`
});

코드는 대략 이렇게 구현되어 있습니다.

대부분의 영상들은 문제없이 실행되고, 컨트롤 할 수 있었으나,
VEVO와 같이 저작권이 강하게 걸려있는 음악영상들은 재생이 안되는 큰 문제가 발생했습니다.

그래서 몇가지 방법을 찾아보니,

  1. 영상 요청시 Referer header 값을 추가하면 된다고 하더군요 참조
    react-native-webview 문서에도 request를 보낼때 header값을 추가할 수 있는 방법이 나와 있지만,
    제가 현재 사용하는 방식인 html 소스를 그대로 갖다 쓰는 방식에서는 사용할 수가 없었습니다.

  2. youtube iframe api를 사용할 때, parameter값으로 origin 값을 추가하는 방식 참조
    을 사용하고자 했으나, 안드로이드 뿐만아니라 웹에서도 적용되지 않았습니다.

참고로, iOS에서 사용하는 react-native-youtube는 origin 파라미터를 제공하여 아래와 같은 방식으로 VEVO영상을 재생할 수 있게 해주고 있습니다.

- (BOOL)webView:(UIWebView *)webView
    shouldStartLoadWithRequest:(NSURLRequest *)request
                navigationType:(UIWebViewNavigationType)navigationType {
  if ([request.URL.host isEqual: self.originURL.host]) {
    return YES;
  } else if ([request.URL.scheme isEqual:@"ytplayer"]) {
    [self notifyDelegateOfYouTubeCallbackUrl:request.URL];
    return NO;
  } else if ([request.URL.scheme isEqual: @"http"] || [request.URL.scheme isEqual:@"https"]) {
    return [self handleHttpNavigationToUrl:request.URL];
  }
  return YES;
}

위에서 찾은 방법들을 통한 간단한 테스트 그리고 react-native-youtube를 참고해보니 VEVO와 같은 영상들은 최초요청할때 정상적인 domain값이 필요하다고 판단을 했습니다.

그런데, html 텍스트를 그대로 가져다 불러오는 저의 상황에서 어떤 부분을 손을 봐야할지 막막합니다.
혹시 해결할 수 있는 힌트를 알고 계신분이 있으시다면 공유해주셨으면 좋겠습니다.

답변 부탁드립니다.
감사합니다.