beomboo

[Android] Exoplayer로 Youtube영상 재생하기 - 1 본문

AOS

[Android] Exoplayer로 Youtube영상 재생하기 - 1

dev_beom_12 2019. 6. 24. 13:25
반응형

2020.05.27

현재 엑소플레이어 재생문제가 많이 발생하고 있어서 아래 깃허브 이슈 게시판을 참고해주시면 감사하겠습니다 ㅎ

https://github.com/google/ExoPlayer/issues

 

google/ExoPlayer

An extensible media player for Android. Contribute to google/ExoPlayer development by creating an account on GitHub.

github.com

 

저는 엑소플레이어 라이브러리를 접목하게된 계기는 

YouTube API를 이용하여 유튜브 영상을 무한 재생을 구현하고 싶었으나 

화면이 멈추는 등 다양한 오류가 발생하였습니다. (오류이미지 첨부못해서 죄송합니다)

몇일간 삽질을 해보았지만 해결되지 않아 엑소플레이어로 바꾸게 되었으며

엑소플레이어는 버퍼링이 없었으며 다소 쉽게 다룰수 있었기에 글로써 남기고 싶었습니다.

 

1. 시작하기

저는 아래의 블로그에 있는 소스코드를 이용하여 초기 프로젝트를 구성하였습니다.

https://androidwave.com/play-youtube-video-in-exoplayer/

 

Play Youtube Video in ExoPlayer - AndroidWave

I have explained how to play youtube video inside ExoPlayer. I have prepare a ExoPlayer Manager utils class for video streaming from server as well.

androidwave.com

- build.gradle (Module:app)

    // For youtube Url Extractor
    implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v1.7.0'
    // ExoPlayer
    implementation 'com.google.android.exoplayer:exoplayer:2.7.3'
    implementation 'com.google.android.exoplayer:exoplayer-core:2.7.3'
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.7.3'
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.7.3'

 

youtubeExtractor 는 Exoplayer가 Youtube 키값을 불러와 변환을 하는 용도로 사용하는것 같아요

로그로 출력을 찍어보면 나름 복잡하게 암호화? 되어서 나오는것을 볼 수 있습니다

// Exoplayer 

주석 아래에 해당하는 것은 원활한 엑소플레이어 재생을 위해 선언을 해두었습니다.

 

2. 실행하기

 - AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />

 - MainActivity.java

public class MainActivity extends AppCompatActivity {
    // Replace video id with your video Id
    static String[] YOUTUBE_VIDEO_ID = new String[]{"Czv7gbz3d7I","ZfpRZGJAo1o"};
    String TAG = "테스트";
    private String BASE_URL = "https://www.youtube.com";
    private String mYoutubeLink;
    static int player_now;
    LinearLayout exo_controller;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 엑소플레이어 하단 컨트롤러 안보이게하기
        //exo_controller = (LinearLayout) findViewById(R.id.exo_controller);
        //exo_controller.setVisibility(View.GONE);

        extractYoutubeUrl(YOUTUBE_VIDEO_ID[0], 0);

        Log.i(TAG, " 재생중인가 " + ExoPlayerManager.getSharedInstance(this).isPlayerPlaying());
    }


    protected void extractYoutubeUrl(final String url, int num) {
        Log.i(TAG, " --------------------- 재생준비 ---------------------");
        final String geturl = url;
        mYoutubeLink = BASE_URL + "/watch?v=" + geturl;
        player_now = num;
        Log.i(TAG, " [유튜브주소변환] geturl= " + geturl + " 들어온값: " + num);
        @SuppressLint("StaticFieldLeak") YouTubeExtractor mExtractor = new YouTubeExtractor(this) {
            @Override
            protected void onExtractionComplete(SparseArray<YtFile> sparseArray, VideoMeta videoMeta) {
                if (sparseArray != null) {
                    playVideo(sparseArray.get(22).getUrl());
                    Log.i(TAG, " --------------------- sparseArray != null ---------------------");
                    Log.i(TAG, " extractYoutubeUrl() param" + "\n videoMeta: " + videoMeta.getTitle() + "\n getUrl: " + sparseArray.get(22).getUrl() + "\n SparseArraySize: " + sparseArray.size() +
                            "\n sparseArray(22).URl: " + sparseArray.get(22).getUrl());
                }
            }
        };
        mExtractor.extract(mYoutubeLink, true, true);
    }

    private void playVideo(String downloadUrl) {
        PlayerView mPlayerView = findViewById(R.id.mPlayerView);
        mPlayerView.setPlayer(ExoPlayerManager.getSharedInstance(MainActivity.this).getPlayerView().getPlayer());
        ExoPlayerManager.getSharedInstance(MainActivity.this).playStream(downloadUrl);


        Log.i(TAG, " --------------------- playVideo ---------------------");
        Log.i(TAG, " playVideo " + ExoPlayerManager.getSharedInstance(this).isPlayerPlaying());
        Log.i(TAG, " playVideo - downloadUrl: " + downloadUrl);
    }


}

 -  ExoPlayerManager.java

public class ExoPlayerManager {

    /**
     * declare some usable variable
     */
    private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
    private static final String TAG = " 테스트 ";
    private static ExoPlayerManager mInstance = null;
    PlayerView mPlayerView;
    DefaultDataSourceFactory dataSourceFactory;
    String uriString = "";
    ArrayList<String> mPlayList = null;
    Integer playlistIndex = 0;
    CallBacks.playerCallBack listner;
    private SimpleExoPlayer mPlayer;


    /**
     * private constructor
     *
     * @param mContext
     */
    private ExoPlayerManager(final Context mContext) {

        TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
        TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
        mPlayer = ExoPlayerFactory.newSimpleInstance(mContext, trackSelector);

        mPlayerView = new PlayerView(mContext);
        mPlayerView.setUseController(true);
        mPlayerView.requestFocus();
        mPlayerView.setPlayer(mPlayer);

        Uri mp4VideoUri = Uri.parse(uriString);

        dataSourceFactory = new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, "androidwave"), BANDWIDTH_METER);

        final MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
                .createMediaSource(mp4VideoUri);
        Log.d(TAG, "--------------------- ExoPlayerManager ---------------------");

        mPlayer.prepare(videoSource);
        mPlayer.addListener(new Player.EventListener() {
            @Override
            public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
                //Log.i(TAG, "onTimelineChanged:.getWindowCount() " + timeline.getWindowCount() + " manifest: " + manifest.toString() + " reason: " + reason);
                //Log.i(TAG, " mPlayer - onTimelineChanged-reason: " + reason);
            }

            @Override
            public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
                //Log.i(TAG, " mPlayer - onTracksChanged: " + trackGroups.toString() + " trackSelections: " + trackSelections.toString());
            }

            @Override
            public void onLoadingChanged(boolean isLoading) {
                //Log.i(TAG, " mPlayer - onLoadingChanged: " + isLoading);
            }

            @Override
            public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
                // playWhenReady = Whether playback will proceed when ready.
                // playbackState =  One of the STATE constants.
                if (playbackState == 1) {
                    Log.d(TAG, " [STATE_IDLE] 플레이어는 재생할 미디어가 없습니다.");
                } else if (playbackState == 2) {
                    Log.d(TAG, " [STATE_BUFFERING] 플레이어는 현재 위치에서 즉시 재생할 수 없습니다. 이 상태는 일반적으로 더 많은 데이터를로드해야 할 때 발생합니다. ");
                } else if (playbackState == 3) {
                    if (playWhenReady) {
                        Log.d(TAG, " [STATE_READY] 플레이어는 현재 위치에서 즉시 재생할 수 있습니다. 플레이어 getPlayWhenReady()가 참이기에 재생이 됩니다.");
                    } else {
                        Log.d(TAG, " [STATE_READY] 플레이어는 현재 위치에서 즉시 재생할 수 있습니다. 플레이어 getPlayWhenReady()가 참이 아니기에 플레이어가 일시 중지됩니다. ");
                    }
                } else if (playbackState == 4) {
                    Log.d(TAG, " [STATE_ENDED] 플레이어가 미디어 재생을 마쳤습니다.");
                }

                if (playbackState == 4 && playWhenReady) {
                    Toast.makeText(mContext, "재생준비완료", Toast.LENGTH_SHORT).show();
                    try {
                        int size = YOUTUBE_VIDEO_ID.length - 1;
                        player_now = player_now + 1;
                        if (player_now > size) {
                            player_now = 0;
                            ((MainActivity) mContext).extractYoutubeUrl(YOUTUBE_VIDEO_ID[0], 0);
                        } else {
                            ((MainActivity) mContext).extractYoutubeUrl(YOUTUBE_VIDEO_ID[player_now], player_now);
                        }
                    } catch (Exception e) {
                        Log.e(TAG, e.toString());
                    }

                }
            }

            @Override
            public void onRepeatModeChanged(int repeatMode) {
                //Log.i(TAG, " mPlayer - onRepeatModeChanged: " + repeatMode);
            }

            @Override
            public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
                //Log.i(TAG, " mPlayer - onShuffleModeEnabledChanged: " + shuffleModeEnabled);
            }

            @Override
            public void onPlayerError(ExoPlaybackException error) {
                //Log.i(TAG, " mPlayer - onPlayerError: " + error);
            }

            @Override
            public void onPositionDiscontinuity(int reason) {
                // Log.i(TAG, " mPlayer - onPositionDiscontinuity: " + reason);
            }

            @Override
            public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
                //Log.i(TAG, " mPlayer - onPlaybackParametersChanged: " + playbackParameters);
            }

            @Override
            public void onSeekProcessed() {
                //Log.i(TAG, " mPlayer - nSeekProcessed: ");
            }
        });
    }

    /**
     * Return ExoPlayerManager instance
     *
     * @param mContext
     * @return
     */
    public static ExoPlayerManager getSharedInstance(Context mContext) {
        if (mInstance == null) {
            mInstance = new ExoPlayerManager(mContext);
        }
        return mInstance;
    }

    public void setPlayerListener(CallBacks.playerCallBack mPlayerCallBack) {
        listner = mPlayerCallBack;
    }

    public PlayerView getPlayerView() {
        return mPlayerView;
    }

    public void playStream(String urlToPlay) {
        uriString = urlToPlay;
        Uri mp4VideoUri = Uri.parse(uriString);
        MediaSource videoSource;
        String filenameArray[] = urlToPlay.split("\\.");
        if (uriString.toUpperCase().contains("M3U8")) {
            videoSource = new HlsMediaSource(mp4VideoUri, dataSourceFactory, null, null);
        } else {
            mp4VideoUri = Uri.parse(urlToPlay);
            videoSource = new ExtractorMediaSource(mp4VideoUri, dataSourceFactory, new DefaultExtractorsFactory(),
                    null, null);
        }
        // Prepare the player with the source.
        mPlayer.prepare(videoSource);
        mPlayer.setPlayWhenReady(true);

    }

    public void setPlayerVolume(float vol) {
        mPlayer.setVolume(vol);
    }

    public void setUriString(String uri) {
        uriString = uri;
    }

    public void setPlaylist(ArrayList<String> uriArrayList, Integer index, CallBacks.playerCallBack callBack) {
        mPlayList = uriArrayList;
        playlistIndex = index;
        listner = callBack;
        playStream(mPlayList.get(playlistIndex));
    }

    public void playerPlaySwitch() {
        if (uriString != "") {
            mPlayer.setPlayWhenReady(!mPlayer.getPlayWhenReady());
        }
    }

    public void stopPlayer(boolean state) {
        mPlayer.setPlayWhenReady(!state);
    }

    public void destroyPlayer() {
        mPlayer.stop();
    }

    public Boolean isPlayerPlaying() {
        return mPlayer.getPlayWhenReady();
    }

    public ArrayList<String> readURLs(String url) {
        if (url == null) return null;
        ArrayList<String> allURls = new ArrayList<String>();
        try {

            URL urls = new URL(url);
            BufferedReader in = new BufferedReader(new InputStreamReader(urls
                    .openStream()));
            String str;
            while ((str = in.readLine()) != null) {
                allURls.add(str);
            }
            in.close();
            return allURls;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

 

 

static String[] YOUTUBE_VIDEO_ID = new String[]{"Czv7gbz3d7I","ZfpRZGJAo1o"};

반복재생을 위해 위와같이 배열로 선언하였으나 입맛에 맞게 SQLite, Array를 이용하여 사용하시면

됩니다.

https://www.youtube.com/watch?v=Czv7gbz3d7I  <-- v= ' 이값 '

정말 중요한 점 알려드리겠습니다.

protected void extractYoutubeUrl(final String url, int num) {
        Log.i(TAG, " --------------------- 재생준비 ---------------------");
        final String geturl = url;
        mYoutubeLink = BASE_URL + "/watch?v=" + geturl;
        player_now = num;
        Log.i(TAG, " [유튜브주소변환] geturl= " + geturl + " 들어온값: " + num);
        @SuppressLint("StaticFieldLeak") YouTubeExtractor mExtractor = new YouTubeExtractor(this) {
            @Override
            protected void onExtractionComplete(SparseArray<YtFile> sparseArray, VideoMeta videoMeta) {
                if (sparseArray != null) {
                    playVideo(sparseArray.get(22).getUrl());
                   Log.i(TAG, " --------------------- sparseArray != null ---------------------");
                   Log.i(TAG, " extractYoutubeUrl() param" + "\n videoMeta: " + videoMeta.getTitle() + "\n getUrl: " + sparseArray.get(22).getUrl() + "\n SparseArraySize: " + sparseArray.size() +
                            "\n sparseArray(22).URl: " + sparseArray.get(22).getUrl());
                }
            }
        };
        mExtractor.extract(mYoutubeLink, true, true);
    }

소스코드 (MainActivity 53~70 line)
playVideo(sparseArray.get(22).getUrl();
해당 부분을 볼 수 있을겁니다. 여기서! sparseArray는 integer값으로 Object를 매핑해주는 HashMap과 유사한 방식이라고 합니다. 자세한건 아래 링크를 통해 봐주세요

 

https://developer88.tistory.com/91

 

SparseArray 가 무엇인가요?

HashMap의 키값으로 Integer를 사용하면, Android스튜디오가 퍼포먼스 향상을 위해서 SparseArray를 사용하라고 하는 것을 본적이 있으신가요? 저도 종종 보게되는데요. 오늘은 이 SparseArray에 대해서 간단히 정..

developer88.tistory.com

SparseArray.get() 에 들어가는 숫자는 해상도를 의미합니다.

22 = 720  / 137 = 1080 / 18 = 480 입니다. 영상에 해당하는 해상도를 어떤걸 해주실지

잘 설정해야합니다. 안그러면 재생이 안되며 null 오류가 발생합니다.

 

https://github.com/HaarigerHarald/android-youtubeExtractor/issues/54

 

ytFiles.get(itag).getUrl() returns null · Issue #54 · HaarigerHarald/android-youtubeExtractor

Hi, I just noticed there's an issue with extracting old youtube videos. For example: I'm trying to download Timbaland - Apologize (2009) and app crashes, java.lang.nullpointerexception: Att...

github.com

 

해당 프로젝트로는 이정도의 오류정도가 염려되네요.

첫 게시글인데 미흡한 내용이 있었을 경우 댓글로 남겨주시면 

숙지하도록 하겠습니다. 감사합니다 ㅎ

 

현재 YouTube Player API & Firebase 연동 후 확장된 프로젝트를 이어 진행중에 있습니다.

관련 내용에 대해서는 모듈화 하여 나타내도록 하겠습니다

반응형
Comments