You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 17:57:01