Android Studio Java中ExoPlayer播放YouTube视频失败的解决方案咨询
问题:ExoPlayer播放YouTube链接失败,无需API实现任意URL播放
我希望在ExoPlayer中播放YouTube链接的视频,但当前代码无法正常运行。尝试过多种方法与库均未满足需求,且不想使用Youtube API。目标是让播放器支持任意YouTube URL播放,请问该如何修改代码或有什么建议?
package com.yazilimevi.youview; import android.os.Bundle; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.fragment.app.Fragment; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.util.Util; import at.huber.youtubeExtractor.VideoMeta; import at.huber.youtubeExtractor.YouTubeExtractor; import at.huber.youtubeExtractor.YtFile; public class Video extends Fragment { PlayerView playerView; SimpleExoPlayer player; boolean playWhenReady = true; int currentWindow = 0; long playbackPosition = 0; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.video, container, false); playerView = view.findViewById(R.id.video_view); initPlayer(); return view; } private void initPlayer() { player = new SimpleExoPlayer.Builder(getContext()).build(); playerView.setPlayer(player); String youtubeUrl = "https://www.youtube.com/watch?v=dxeKPU0MBU8"; String videoId = extractVideoId(youtubeUrl); if (videoId != null) { playYoutubeVideo(videoId); System.out.println("YouTube Video ID: " + videoId); } else { // 未能提取有效的视频ID System.out.println("未能提取有效的YouTube视频ID。"); } player.prepare(); // 如果没有这行请添加 } private String extractVideoId(String youtubeUrl) { String videoId = null; String pattern = "(?<=watch\\?v=|/videos/|embed\\/|youtu.be\\/|\\/v\\/|\\/e\\/|watch\\?v%3D|watch\\?feature=player_embedded&v=|%2Fvideos%2F|embed%u200C2F|youtu.be%2F|%2Fv%2F)[^#\\&\\?\\n]*"; String[] patterns = { "^(https?\\:\/\\/)?(www\\.)?(youtube\\.com|youtu\\.?be)\\/(watch\\?v=|embed\\/|v\\/|e\\/|watch\\?feature=player_embedded&v=|%2Fvideos%2F|embed%u200C2F|youtu.be%2F|%2Fv%2F)[^#\\&\\?\\n]*[^#?\\n]*", "^https?\\:\/\\/(www\\.)?youtu\\.be\\/([^#\\&\\?\\n]*)", "^https?\\:\/\\/www\\.youtube\\.com\\/v\\/([^#\\&\\?\\n]*)", "^https?\\:\/\\/www\\.youtube\\.com\\/user\\/[^\\?\\n]*", "^https?\\:\/\\/www\\.youtube\\.com\\/[^\\&\\?\\n]*" }; for (String p : patterns) { if (youtubeUrl != null && youtubeUrl.trim().length() > 0 && youtubeUrl.matches(p)) { String[] splitArr = youtubeUrl.split("[^\\w-]"); for (String split : splitArr) { if (split.length() == 11) { videoId = split; break; } } } } return videoId; } private void playYoutubeVideo(String videoId) { /* new YouTubeExtractor(getContext()) { @Override protected void onExtractionComplete(SparseArray<YtFile> ytFiles, VideoMeta videoMeta) { if (ytFiles != null) { int videoTag = 137; int audioTag = 140; MediaSource audioSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(ytFiles.get(audioTag).getUrl())); MediaSource videoSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(ytFiles.get(videoTag).getUrl())); player.setMediaSource(new MergingMediaSource( true, videoSource, audioSource), true ); player.prepare(); player.setPlayWhenReady(playWhenReady); player.seekTo(currentWindow, playbackPosition); } else { System.out.println("ytFiles为空"); } } }.extract("dxeKPU0MBU8" , false, false);*/ new YouTubeExtractor(getContext()) { @Override protected void onExtractionComplete(SparseArray<YtFile> ytFiles, VideoMeta videoMeta) { try { if (ytFiles != null && ytFiles.size() > 0) { int videoTag = 137; int audioTag = 140; MediaSource audioSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(ytFiles.get(audioTag).getUrl())); MediaSource videoSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(ytFiles.get(videoTag).getUrl())); player.setMediaSource(new MergingMediaSource( true, videoSource, audioSource), true ); player.prepare(); player.setPlayWhenReady(playWhenReady); player.seekTo(currentWindow, playbackPosition); } else { logErrorSources(ytFiles, videoMeta); String errorMessage = getSpecificErrorMessage(null); // 无异常时调用 System.out.println("YouTubeExtractor错误:无法获取视频文件。 " + errorMessage); } } catch (Exception e) { logErrorSources(ytFiles, videoMeta); String errorMessage = getSpecificErrorMessage(e); // 有异常时调用 System.out.println("YouTubeExtractor错误:无法获取视频文件。 " + errorMessage); } } }.extract("https://www.youtube.com/watch?v=_xpk2c6W8wA", false, false); } private void logErrorSources(SparseArray<YtFile> ytFiles, VideoMeta videoMeta) { System.out.println("错误来源:"); if (ytFiles == null) { System.out.println("ytFiles: null"); } else { System.out.println("ytFiles Size: " + ytFiles.size()); for (int i = 0; i < ytFiles.size(); i++) { int key = ytFiles.keyAt(i); YtFile ytFile = ytFiles.get(key); System.out.println("YtFile Key: " + key + ", Url: " + (ytFile != null ? ytFile.getUrl() : "null")); } } if (videoMeta == null) { System.out.println("videoMeta: null"); } else { System.out.println("videoMeta Video ID: " + videoMeta.getVideoId()); // 如需记录其他videoMeta属性可在此添加 } } private String getSpecificErrorMessage(Exception exception) { if (exception != null) { return exception.getMessage(); // 获取异常自带信息 } else { return "发生未知错误。"; } } @Override public void onStart() { super.onStart(); if (Util.SDK_INT >= 24) { initPlayer(); } } @Override public void onStop() { if (Util.SDK_INT >= 24) { releasePlayer(); } super.onStop(); } private void releasePlayer() { if (player != null) { playWhenReady = player.getPlayWhenReady(); playbackPosition = player.getCurrentPosition(); currentWindow = player.getCurrentWindowIndex(); player.release(); player = null; } } @Override public void onResume() { super.onResume(); if (Util.SDK_INT < 24 || player == null) { initPlayer(); hideSystemUI(); } } private void hideSystemUI() { playerView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION ); } @Override public void onPause() { if (Util.SDK_INT < 24) { releasePlayer(); } super.onPause(); } }
修复建议与优化方案
1. 修复playYoutubeVideo硬编码问题
当前方法接收videoId参数但实际用了固定URL,导致无法播放指定视频,修改如下:
private void playYoutubeVideo(String videoId) { new YouTubeExtractor(getContext()) { @Override protected void onExtractionComplete(SparseArray<YtFile> ytFiles, VideoMeta videoMeta) { try { if (ytFiles != null && ytFiles.size() > 0) { // 原有逻辑保持不变 } else { // 错误处理不变 } } catch (Exception e) { // 异常处理不变 } } }.extract("https://www.youtube.com/watch?v=" + videoId, false, false); }
2. 移除提前调用的player.prepare()
initPlayer中提前调用player.prepare()会导致播放器无媒体源就进入准备状态,后续设置的媒体源无法生效,直接删除该行代码,媒体源设置完成后在回调里调用prepare即可。
3. 增加视频/音频格式降级逻辑
固定使用tag 137(1080p)和140(AAC音频)会导致部分视频无对应格式时播放失败,改为遍历选择可用格式:
// 替换原固定tag选择逻辑 YtFile videoFile = null; // 优先选高分辨率,依次降级 int[] preferredVideoTags = {137, 136, 135, 134, 133}; for (int tag : preferredVideoTags) { if (ytFiles.get(tag) != null) { videoFile = ytFiles.get(tag); break; } } YtFile audioFile = null; // 优先选高质量音频,依次降级 int[] preferredAudioTags = {140, 141, 171, 143}; for (int tag : preferredAudioTags) { if (ytFiles.get(tag) != null) { audioFile = ytFiles.get(tag); break; } } if (videoFile != null && audioFile != null) { MediaSource audioSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(audioFile.getUrl())); MediaSource videoSource = new ProgressiveMediaSource .Factory(new DefaultHttpDataSource.Factory()) .createMediaSource(MediaItem.fromUri(videoFile.getUrl())); player.setMediaSource(new MergingMediaSource(true, videoSource, audioSource), true); player.prepare(); player.setPlayWhenReady(playWhenReady); player.seekTo(currentWindow, playbackPosition); } else { System.out.println("无法找到可用的视频或音频流"); }
4. 简化视频ID提取逻辑
原正则匹配过于复杂,改用更可靠的分支判断:
private String extractVideoId(String youtubeUrl) { if (youtubeUrl == null || youtubeUrl.isEmpty()) return null; // 处理youtu.be短链接 if (youtubeUrl.contains("youtu.be/")) { return youtubeUrl.split("youtu.be/")[1].split("[?&]")[0]; } // 处理watch?v=格式 if (youtubeUrl.contains("watch?v=")) { return youtubeUrl.split("watch?v=")[1].split("[?&]")[0]; } // 处理embed格式 if (youtubeUrl.contains("embed/")) { return youtubeUrl.split("embed/")[1].split("[?&]")[0]; } // 处理/v/格式 if (youtubeUrl.contains("/v/")) { return youtubeUrl.split("/v/")[1].split("[?&]")[0]; } return null; }
5. 检查权限与依赖
- 确保
AndroidManifest.xml添加网络权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- 确认
youtubeExtractor为最新版本,在build.gradle中更新依赖:
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:2.1.0'
6. 修复生命周期重复初始化问题
当前多个生命周期方法调用initPlayer导致播放器重复创建,修改为仅在必要时初始化:
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.video, container, false); playerView = view.findViewById(R.id.video_view); // 移除这里的initPlayer调用,交给生命周期方法处理 return view; } @Override public void onStart() { super.onStart(); if (Util.SDK_INT >= 24) { initPlayer(); } } @Override public void onResume() { super.onResume(); hideSystemUI(); if (Util.SDK_INT < 24 || player == null) { initPlayer(); } }
注意事项
- YouTube流媒体链接会定期失效,第三方提取库需要持续更新才能适配接口变化,提取失败时优先检查库版本。
- 该方式违反YouTube服务条款,使用前请评估合规风险。
内容的提问来源于stack exchange,提问作者AHMET YILDIZBAKAN
相关产品推荐
相关产品推荐

