Repository navigation
Improve channel menu, background startup refresh, and 4K hardware decoding #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
12f960d
Make the channel menu easier to navigate on a remote
yunnysunny 651f096
Refresh the M3U source in the background on startup
yunnysunny 933dc72
Keep 4K HEVC on the hardware decoder when only the level is too high
yunnysunny 9a31ae0
Treat an M3U refresh with no channels as a failed refresh
yunnysunny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 154 additions & 0 deletions
154
app/src/main/java/com/whyun/witv/player/LevelTolerantVideoRenderer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package com.whyun.witv.player; | ||
|
|
||
| import android.content.Context; | ||
| import android.media.MediaCodecInfo.CodecProfileLevel; | ||
| import android.util.Log; | ||
| import android.util.Pair; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.OptIn; | ||
| import androidx.annotation.VisibleForTesting; | ||
| import androidx.media3.common.C; | ||
| import androidx.media3.common.Format; | ||
| import androidx.media3.common.util.UnstableApi; | ||
| import androidx.media3.exoplayer.ExoPlaybackException; | ||
| import androidx.media3.exoplayer.ForwardingRenderer; | ||
| import androidx.media3.exoplayer.Renderer; | ||
| import androidx.media3.exoplayer.RendererCapabilities; | ||
| import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; | ||
| import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; | ||
| import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * 包住 {@code MediaCodecVideoRenderer}:码流只是 <b>Level</b> 标得比硬解声明的高时,仍然报告「支持」, | ||
| * 让硬解在轨道选择里胜过 FFmpeg 软解。 | ||
| * | ||
| * <p>实测:CCTV-4K 组播标的是 {@code hvc1.2.4.L180}(HEVC Main10 Level 6.0),而 S905X 的 | ||
| * {@code OMX.amlogic.hevc.decoder.awesome} 只声明到 Level 5.1。硬解渲染器于是报 | ||
| * {@code FORMAT_EXCEEDS_CAPABILITIES},FFmpeg 渲染器报 {@code FORMAT_HANDLED},轨道选择取支持度更高的, | ||
| * 视频就落到了 CPU 软解 4K 10bit 上,持续大量丢帧。可同一个台在「仅硬解」下播放正常—— | ||
| * 编码器的 Level 往往标得保守,实际码率、分辨率仍在硬解能力内。 | ||
| * | ||
| * <p>只放宽 Level:mime、Profile 必须被某个硬件解码器声明支持,去掉 codecs 串后的分辨率/帧率也必须在它的 | ||
| * 能力内;Profile 不支持(比如解码器根本不支持 10bit)等真正解不了的情况照旧交给 FFmpeg 补位。 | ||
| */ | ||
| @OptIn(markerClass = UnstableApi.class) | ||
| final class LevelTolerantVideoRenderer extends ForwardingRenderer { | ||
|
|
||
| private static final String TAG = "LevelTolerantVideo"; | ||
|
|
||
| private final RendererCapabilities capabilities; | ||
|
|
||
| LevelTolerantVideoRenderer(@NonNull Renderer renderer, @NonNull Context context, | ||
| @NonNull MediaCodecSelector mediaCodecSelector) { | ||
| super(renderer); | ||
| capabilities = new Capabilities(renderer.getCapabilities(), context, mediaCodecSelector); | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public RendererCapabilities getCapabilities() { | ||
| return capabilities; | ||
| } | ||
|
|
||
| /** | ||
| * 硬解只是因为 Level 不够才被判为超出能力时返回 true。 | ||
| */ | ||
| @VisibleForTesting | ||
| static boolean exceedsOnlyByLevel(@NonNull Context context, | ||
| @NonNull MediaCodecSelector mediaCodecSelector, | ||
| @NonNull Format format) | ||
| throws MediaCodecUtil.DecoderQueryException { | ||
| String mimeType = format.sampleMimeType; | ||
| if (mimeType == null || format.codecs == null || format.drmInitData != null) { | ||
| return false; | ||
| } | ||
| Pair<Integer, Integer> profileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); | ||
| if (profileAndLevel == null) { | ||
| return false; | ||
| } | ||
| int profile = profileAndLevel.first; | ||
| // 去掉 codecs 串后,isFormatSupported 就不再检查 Profile/Level,只看 mime 和分辨率/帧率 | ||
| Format withoutCodecs = format.buildUpon().setCodecs(null).build(); | ||
| List<MediaCodecInfo> decoderInfos = | ||
| mediaCodecSelector.getDecoderInfos(mimeType, false, false); | ||
| for (MediaCodecInfo info : decoderInfos) { | ||
| if (info.hardwareAccelerated | ||
| && supportsProfile(info, profile) | ||
| && info.isFormatSupported(context, withoutCodecs)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static boolean supportsProfile(MediaCodecInfo info, int profile) { | ||
| for (CodecProfileLevel profileLevel : info.getProfileLevels()) { | ||
| if (profileLevel.profile == profile) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static final class Capabilities implements RendererCapabilities { | ||
|
|
||
| private final RendererCapabilities delegate; | ||
| private final Context context; | ||
| private final MediaCodecSelector mediaCodecSelector; | ||
|
|
||
| Capabilities(RendererCapabilities delegate, Context context, | ||
| MediaCodecSelector mediaCodecSelector) { | ||
| this.delegate = delegate; | ||
| this.context = context; | ||
| this.mediaCodecSelector = mediaCodecSelector; | ||
| } | ||
|
|
||
| @NonNull | ||
| @Override | ||
| public String getName() { | ||
| return delegate.getName(); | ||
| } | ||
|
|
||
| @Override | ||
| public int getTrackType() { | ||
| return delegate.getTrackType(); | ||
| } | ||
|
|
||
| @Override | ||
| public int supportsFormat(@NonNull Format format) throws ExoPlaybackException { | ||
| int support = delegate.supportsFormat(format); | ||
| if (RendererCapabilities.getFormatSupport(support) != C.FORMAT_EXCEEDS_CAPABILITIES) { | ||
| return support; | ||
| } | ||
| try { | ||
| if (!exceedsOnlyByLevel(context, mediaCodecSelector, format)) { | ||
| return support; | ||
| } | ||
| } catch (MediaCodecUtil.DecoderQueryException e) { | ||
| Log.w(TAG, "Decoder query failed, keep original support", e); | ||
| return support; | ||
| } | ||
| Log.i(TAG, "Only codec level exceeds hardware decoder, treat as handled: " | ||
| + format.codecs); | ||
| return (support & ~FORMAT_SUPPORT_MASK) | C.FORMAT_HANDLED; | ||
| } | ||
|
|
||
| @Override | ||
| public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { | ||
| return delegate.supportsMixedMimeTypeAdaptation(); | ||
| } | ||
|
|
||
| @Override | ||
| public void setListener(@NonNull Listener listener) { | ||
| delegate.setListener(listener); | ||
| } | ||
|
|
||
| @Override | ||
| public void clearListener() { | ||
| delegate.clearListener(); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a stream genuinely exceeds a decoder's advertised level, matching the profile and resolution/frame rate does not establish that the decoder can handle it: codec levels also constrain properties such as bitrate, sample rate, and decoded-picture buffering. This loop therefore promotes every over-level AVC/HEVC/etc. format to
FORMAT_HANDLED, causing the hardware renderer to win over FFmpeg even on devices whose level declaration is accurate; those streams can then fail or stutter instead of using the previously selected software fallback. Limit the workaround to known affected codecs/decoders or otherwise verify the constraints represented by the level before returning true.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不修改。确实存在码流真超出 Level、硬解却被判为支持的情况,但只在 1080p 及以下才有实际影响,因为 4K 交给软解在电视盒子上本来就跑不动。会出现这种情况的是硬解只声明到 Level 4.1 一类的老设备,年代已经比较久,项目不再支持,所以保留目前的放宽条件。