Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbbfa4e26 | ||
|
|
050110922c | ||
|
|
f97db4643e | ||
|
|
1715dc02b7 | ||
|
|
23c8b42e79 | ||
|
|
3ddcf4d284 | ||
|
|
6a97e9b05b | ||
|
|
6af1df5122 | ||
|
|
68f0edca71 | ||
|
|
f5fda8c7de | ||
|
|
9ea2b81b28 | ||
|
|
c568862ee1 | ||
|
|
42b767f619 | ||
|
|
ebf7fc4244 | ||
|
|
102642d58f | ||
|
|
5115e1181e | ||
|
|
f70ac357b6 | ||
|
|
7136341c3c |
5
.github/workflows/app-build.yaml
vendored
5
.github/workflows/app-build.yaml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -13,7 +16,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
|
||||
10
.github/workflows/app-lint.yaml
vendored
10
.github/workflows/app-lint.yaml
vendored
@@ -7,14 +7,20 @@ on:
|
||||
- release-*
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
@@ -22,7 +28,7 @@ jobs:
|
||||
- name: Run detekt and lint task
|
||||
run: ./gradlew --build-cache --no-daemon --info detekt lint
|
||||
- name: Upload SARIF files
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
sarif_file: .
|
||||
|
||||
4
.github/workflows/app-publish.yaml
vendored
4
.github/workflows/app-publish.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- name: Upload release archive to GitHub release
|
||||
uses: alexellis/upload-assets@0.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
with:
|
||||
asset_paths: '["build/jellyfin-publish/*"]'
|
||||
- name: Upload release archive to repo.jellyfin.org
|
||||
|
||||
5
.github/workflows/app-test.yaml
vendored
5
.github/workflows/app-test.yaml
vendored
@@ -7,6 +7,9 @@ on:
|
||||
- release-*
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -14,7 +17,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: adopt
|
||||
java-version: 11
|
||||
|
||||
3
.github/workflows/gradlew-validate.yaml
vendored
3
.github/workflows/gradlew-validate.yaml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
paths:
|
||||
- '**/gradlе-wrapper.jar'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<application
|
||||
android:name=".JellyfinApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@drawable/app_banner"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.androidtv.preference.constant.RatingType
|
||||
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.defaultAudioBehavior
|
||||
import org.jellyfin.androidtv.util.DeviceUtils
|
||||
@@ -90,9 +91,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
var videoPlayer = Preference.enum("video_player", PreferredVideoPlayer.EXOPLAYER)
|
||||
|
||||
/**
|
||||
* Enable refresh rate switching when device supports it
|
||||
* Change refresh rate to match media when device supports it
|
||||
*/
|
||||
var refreshRateSwitchingEnabled = Preference.boolean("pref_refresh_switching", false)
|
||||
var refreshRateSwitchingBehavior = Preference.enum("refresh_rate_switching_behavior", RefreshRateSwitchingBehavior.DISABLED)
|
||||
|
||||
/**
|
||||
* Send a path instead to the external player
|
||||
@@ -230,6 +231,15 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
// Disable AC3 (Dolby Digital) on Fire Stick Gen 1 devices
|
||||
if (DeviceUtils.isFireTvStickGen1()) putBoolean("pref_bitstream_ac3", false)
|
||||
}
|
||||
|
||||
// v0.13.3 to v0.13.4
|
||||
migration(toVersion = 6) {
|
||||
putEnum("refresh_rate_switching_behavior",
|
||||
when {
|
||||
it.getBoolean("pref_refresh_switching", false) -> RefreshRateSwitchingBehavior.SCALE_ON_TV
|
||||
else -> RefreshRateSwitchingBehavior.DISABLED
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.EnumDisplayOptions
|
||||
|
||||
enum class RefreshRateSwitchingBehavior {
|
||||
@EnumDisplayOptions(R.string.state_disabled)
|
||||
DISABLED,
|
||||
|
||||
/**
|
||||
* When comparing modes, use difference in resolution to rank modes.
|
||||
*/
|
||||
@EnumDisplayOptions(R.string.pref_refresh_rate_scale_on_tv)
|
||||
SCALE_ON_TV,
|
||||
|
||||
/**
|
||||
* When comparing modes, rank native resolution modes highest.
|
||||
* Otherwise use difference in resolution to rank modes.
|
||||
*/
|
||||
@EnumDisplayOptions(R.string.pref_refresh_rate_scale_on_device)
|
||||
SCALE_ON_DEVICE,
|
||||
}
|
||||
@@ -44,10 +44,12 @@ class ExpandableTextView(context: Context, attrs: AttributeSet?) : AppCompatText
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
isFocusable = !text.isNullOrBlank()
|
||||
isClickable = !text.isNullOrBlank()
|
||||
// Only make focusable when text is ellipsized.
|
||||
val isEllipsized = !text.isNullOrBlank() && lineCount > 0 && layout.getEllipsisCount(lineCount - 1) > 0
|
||||
isFocusable = isEllipsized
|
||||
isClickable = isEllipsized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
@@ -622,7 +623,7 @@ public class ItemListActivity extends FragmentActivity {
|
||||
@Override
|
||||
public void onResponse(UserItemDataDto response) {
|
||||
mBaseItem.setUserData(response);
|
||||
((TextUnderButton)v).setImageResource(response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart);
|
||||
((ImageButton)v).setImageResource(response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart);
|
||||
dataRefreshService.getValue().setLastFavoriteUpdate(System.currentTimeMillis());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1483,6 +1483,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
|
||||
// Encode whitespace as html entities
|
||||
final String htmlText = text
|
||||
.replaceAll("\\r\\n", "<br>")
|
||||
.replaceAll("\\n", "<br>")
|
||||
.replaceAll("\\\\h", " ");
|
||||
|
||||
final SpannableString span = new SpannableString(TextUtilsKt.toHtmlSpanned(htmlText));
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.preference.UserSettingPreferences;
|
||||
import org.jellyfin.androidtv.preference.constant.NextUpBehavior;
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer;
|
||||
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior;
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.util.DeviceUtils;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
@@ -115,15 +116,18 @@ public class PlaybackController {
|
||||
private long lastPlaybackError = 0;
|
||||
|
||||
private Display.Mode[] mDisplayModes;
|
||||
private boolean refreshRateSwitchingEnabled;
|
||||
private RefreshRateSwitchingBehavior refreshRateSwitchingBehavior = RefreshRateSwitchingBehavior.DISABLED;
|
||||
|
||||
public PlaybackController(List<BaseItemDto> items, CustomPlaybackOverlayFragment fragment) {
|
||||
mItems = items;
|
||||
mFragment = fragment;
|
||||
mHandler = new Handler();
|
||||
|
||||
refreshRateSwitchingEnabled = DeviceUtils.is60() && userPreferences.getValue().get(UserPreferences.Companion.getRefreshRateSwitchingEnabled());
|
||||
if (refreshRateSwitchingEnabled) getDisplayModes();
|
||||
if (DeviceUtils.is60()) {
|
||||
refreshRateSwitchingBehavior = userPreferences.getValue().get(UserPreferences.Companion.getRefreshRateSwitchingBehavior());
|
||||
if (refreshRateSwitchingBehavior != RefreshRateSwitchingBehavior.DISABLED)
|
||||
getDisplayModes();
|
||||
}
|
||||
|
||||
// Set default value for useVlc field
|
||||
// when set to auto the default will be exoplayer
|
||||
@@ -214,7 +218,7 @@ public class PlaybackController {
|
||||
}
|
||||
|
||||
public int getSubtitleStreamIndex() {
|
||||
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : mDefaultSubIndex;
|
||||
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : -1;
|
||||
}
|
||||
|
||||
public List<SubtitleStreamInfo> getSubtitleStreams() {
|
||||
@@ -296,7 +300,7 @@ public class PlaybackController {
|
||||
mDisplayModes = display.getSupportedModes();
|
||||
Timber.i("** Available display refresh rates:");
|
||||
for (Display.Mode mDisplayMode : mDisplayModes) {
|
||||
Timber.i("%f", mDisplayMode.getRefreshRate());
|
||||
Timber.d("display mode %s - %dx%d@%f", mDisplayMode.getModeId(), mDisplayMode.getPhysicalWidth(), mDisplayMode.getPhysicalHeight(), mDisplayMode.getRefreshRate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,8 +315,10 @@ public class PlaybackController {
|
||||
|
||||
Display.Mode defaultMode = mFragment.requireActivity().getWindowManager().getDefaultDisplay().getMode();
|
||||
|
||||
Timber.d("trying to find display mode for video: %sx%s @%sfps", videoStream.getWidth(), videoStream.getHeight(), videoStream.getRealFrameRate());
|
||||
Timber.d("trying to find display mode for video: %dx%d@%f", videoStream.getWidth(), videoStream.getHeight(), videoStream.getRealFrameRate());
|
||||
for (Display.Mode mode : mDisplayModes) {
|
||||
Timber.d("considering display mode: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
|
||||
// Skip unwanted display modes
|
||||
if (mode.getPhysicalWidth() < 1280 || mode.getPhysicalHeight() < 720) // Skip non-HD
|
||||
continue;
|
||||
@@ -324,18 +330,28 @@ public class PlaybackController {
|
||||
if (rate != sourceRate && rate != sourceRate * 2 && rate != Math.round(sourceRate * 2.5)) // Skip inappropriate rates
|
||||
continue;
|
||||
|
||||
Timber.d("qualifying display mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
Timber.d("qualifying display mode: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
|
||||
int resolutionDifference = 0;
|
||||
if (!(mode.getPhysicalWidth() == defaultMode.getPhysicalWidth() && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight()))
|
||||
resolutionDifference = mode.getPhysicalWidth() - videoStream.getWidth();
|
||||
// if scaling on-device, keep native resolution modes at diff 0 (best score)
|
||||
// for other resolutions when scaling on device, or if scaling on tv, score based on distance from media resolution
|
||||
|
||||
// use -1 as the default so, with SCALE_ON_DEVICE, a mode at native resolution will rank higher than
|
||||
// a mode with equal refresh rate and the same resolution as the media
|
||||
int resolutionDifference = -1;
|
||||
if ((refreshRateSwitchingBehavior == RefreshRateSwitchingBehavior.SCALE_ON_DEVICE &&
|
||||
!(mode.getPhysicalWidth() == defaultMode.getPhysicalWidth() && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight())) ||
|
||||
|
||||
refreshRateSwitchingBehavior == RefreshRateSwitchingBehavior.SCALE_ON_TV) {
|
||||
|
||||
resolutionDifference = Math.abs(mode.getPhysicalWidth() - videoStream.getWidth());
|
||||
}
|
||||
int refreshRateDifference = rate - sourceRate;
|
||||
|
||||
// use 100,000 to account for refresh rates 120Hz+ (at 120Hz rate == 12,000)
|
||||
int weight = 100000 - refreshRateDifference + 100000 - resolutionDifference;
|
||||
|
||||
if (weight > curWeight) {
|
||||
Timber.d("preferring mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
Timber.d("preferring mode: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
|
||||
curWeight = weight;
|
||||
bestMode = mode;
|
||||
}
|
||||
@@ -357,7 +373,8 @@ public class PlaybackController {
|
||||
Timber.i("*** Best refresh mode is: %s - %dx%d/%f",
|
||||
best.getModeId(), best.getPhysicalWidth(), best.getPhysicalHeight(), best.getRefreshRate());
|
||||
if (current.getModeId() != best.getModeId()) {
|
||||
Timber.i("*** Attempting to change refresh rate from %s/%s", current.getModeId(), current.getRefreshRate());
|
||||
Timber.i("*** Attempting to change refresh rate from: %s - %dx%d@%f", current.getModeId(), current.getPhysicalWidth(),
|
||||
current.getPhysicalHeight(),current.getRefreshRate());
|
||||
WindowManager.LayoutParams params = mFragment.requireActivity().getWindow().getAttributes();
|
||||
params.preferredDisplayModeId = best.getModeId();
|
||||
mFragment.requireActivity().getWindow().setAttributes(params);
|
||||
@@ -365,7 +382,7 @@ public class PlaybackController {
|
||||
Timber.i("Display is already in best mode");
|
||||
}
|
||||
} else {
|
||||
Timber.i("*** Unable to find display mode for refresh rate: %s", videoStream.getRealFrameRate());
|
||||
Timber.i("*** Unable to find display mode for refresh rate: %f", videoStream.getRealFrameRate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,8 +421,8 @@ public class PlaybackController {
|
||||
play(position, null);
|
||||
}
|
||||
|
||||
private void play(long position, @Nullable Integer transcodedSubtitle) {
|
||||
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, transcodedSubtitle);
|
||||
private void play(long position, @Nullable Integer forcedSubtitleIndex) {
|
||||
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);
|
||||
|
||||
if (position < 0) {
|
||||
Timber.i("Negative start requested - adjusting to zero");
|
||||
@@ -514,8 +531,8 @@ public class PlaybackController {
|
||||
vlcOptions.setEnableDirectStream(false);
|
||||
vlcOptions.setEnableDirectPlay(false);
|
||||
}
|
||||
vlcOptions.setSubtitleStreamIndex(transcodedSubtitle);
|
||||
vlcOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
|
||||
vlcOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
|
||||
vlcOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
|
||||
DeviceProfile vlcProfile = new LibVlcProfile(isLiveTv);
|
||||
vlcOptions.setProfile(vlcProfile);
|
||||
|
||||
@@ -526,8 +543,8 @@ public class PlaybackController {
|
||||
if (exoErrorEncountered || (isLiveTv && !directStreamLiveTv))
|
||||
internalOptions.setEnableDirectStream(false);
|
||||
internalOptions.setMaxAudioChannels(Utils.downMixAudio() ? 2 : null); //have to downmix at server
|
||||
internalOptions.setSubtitleStreamIndex(transcodedSubtitle);
|
||||
internalOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
|
||||
internalOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
|
||||
internalOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
|
||||
DeviceProfile internalProfile = new BaseProfile();
|
||||
if (DeviceUtils.is60() || userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled())) {
|
||||
boolean hlsSupported = false;
|
||||
@@ -672,15 +689,22 @@ public class PlaybackController {
|
||||
if (mVideoManager == null)
|
||||
return;
|
||||
mVideoManager.init(getBufferAmount(), useDeinterlacing);
|
||||
|
||||
Timber.d("server default: %s inferred first track: %s", internalResponse.getMediaSource().getDefaultAudioStreamIndex(), bestGuessAudioTrack(internalResponse.getMediaSource()));
|
||||
mCurrentOptions = useVlc ? vlcOptions : internalOptions;
|
||||
startItem(item, position, useVlc ? vlcResponse : internalResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Unable to get internal stream info");
|
||||
Timber.e(exception, "Unable to get stream info for internal player - falling back to libVLC");
|
||||
if (mVideoManager == null)
|
||||
return;
|
||||
|
||||
boolean useDeinterlacing = vlcResponse.getMediaSource().getVideoStream() != null &&
|
||||
vlcResponse.getMediaSource().getVideoStream().getIsInterlaced() &&
|
||||
(vlcResponse.getMediaSource().getVideoStream().getWidth() == null ||
|
||||
vlcResponse.getMediaSource().getVideoStream().getWidth() > 1200);
|
||||
|
||||
mVideoManager.init(getBufferAmount(), useDeinterlacing);
|
||||
mCurrentOptions = vlcOptions;
|
||||
startItem(item, position, vlcResponse);
|
||||
}
|
||||
@@ -734,15 +758,14 @@ public class PlaybackController {
|
||||
|
||||
mCurrentStreamInfo = response;
|
||||
|
||||
// set the audio index so that it is preserved if the stream is rebuilt when seeking
|
||||
mCurrentOptions.setAudioStreamIndex(response.getMediaSource().getDefaultAudioStreamIndex());
|
||||
// set media source Id in case we need to switch to transcoding
|
||||
mCurrentOptions.setMediaSourceId(response.getMediaSource().getId());
|
||||
setDefaultAudioIndex(response);
|
||||
|
||||
|
||||
// get subtitle info
|
||||
mSubtitleStreams = response.GetSubtitleProfiles(false, apiClient.getValue().getApiUrl(), apiClient.getValue().getAccessToken());
|
||||
mDefaultSubIndex = response.getMediaSource().getDefaultSubtitleStreamIndex() != null ? response.getMediaSource().getDefaultSubtitleStreamIndex() : mDefaultSubIndex;
|
||||
setDefaultAudioIndex(response);
|
||||
Timber.d("default audio index set to %s remote default %s", mDefaultAudioIndex, response.getMediaSource().getDefaultAudioStreamIndex());
|
||||
Timber.d("default sub index set to %s remote default %s", mDefaultSubIndex, response.getMediaSource().getDefaultSubtitleStreamIndex());
|
||||
|
||||
// if burning in, set the subtitle index and the burningSubs flag so that onPrepared and switchSubtitleStream will know that we already have subtitles enabled
|
||||
@@ -752,6 +775,8 @@ public class PlaybackController {
|
||||
mCurrentOptions.setSubtitleStreamIndex(mDefaultSubIndex);
|
||||
Timber.d("stream started with burnt in subs");
|
||||
burningSubs = true;
|
||||
} else {
|
||||
mCurrentOptions.setSubtitleStreamIndex(null);
|
||||
}
|
||||
|
||||
Long mbPos = position * 10000;
|
||||
@@ -770,7 +795,7 @@ public class PlaybackController {
|
||||
}
|
||||
|
||||
// set refresh rate
|
||||
if (refreshRateSwitchingEnabled) {
|
||||
if (refreshRateSwitchingBehavior != RefreshRateSwitchingBehavior.DISABLED) {
|
||||
setRefreshRate(response.getMediaSource().getVideoStream());
|
||||
}
|
||||
|
||||
@@ -829,17 +854,21 @@ public class PlaybackController {
|
||||
}
|
||||
|
||||
public int getAudioStreamIndex() {
|
||||
int currIndex = mDefaultAudioIndex;
|
||||
int currIndex = -1;
|
||||
|
||||
// if not transcoding, libVLC & exoplayer are able to switch tracks and report their selected track
|
||||
if (hasInitializedVideoManager() && !isTranscoding() && mVideoManager.getVLCAudioTrack() > -1) {
|
||||
currIndex = mVideoManager.getVLCAudioTrack();
|
||||
} else if (hasInitializedVideoManager() && !isTranscoding() && isNativeMode() && mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams()) > -1) {
|
||||
currIndex = mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams());
|
||||
} else if (!isTranscoding() && isNativeMode() && mCurrentOptions.getAudioStreamIndex() != null) {
|
||||
// Use stream index from mCurrentOptions if it's set.
|
||||
// This should be null until the player has been queried at least once after playback starts
|
||||
//
|
||||
// Use DefaultAudioStreamIndex for transcoding since they are encoded with only one stream
|
||||
//
|
||||
// Otherwise, query the players
|
||||
if (mCurrentOptions.getAudioStreamIndex() != null) {
|
||||
currIndex = mCurrentOptions.getAudioStreamIndex();
|
||||
} else if (getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
|
||||
} else if (isTranscoding() && getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
|
||||
currIndex = getCurrentMediaSource().getDefaultAudioStreamIndex();
|
||||
} else if (hasInitializedVideoManager() && !isTranscoding()) {
|
||||
currIndex = isNativeMode() ? mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams()) :
|
||||
mVideoManager.getVLCAudioTrack(getCurrentlyPlayingItem().getMediaStreams());
|
||||
}
|
||||
return currIndex;
|
||||
}
|
||||
@@ -864,8 +893,13 @@ public class PlaybackController {
|
||||
if (mDefaultAudioIndex != -1)
|
||||
return;
|
||||
|
||||
if (bestGuessAudioTrack(info.getMediaSource()) != null)
|
||||
mDefaultAudioIndex = bestGuessAudioTrack(info.getMediaSource());
|
||||
Integer remoteDefault = info.getMediaSource().getDefaultAudioStreamIndex();
|
||||
Integer bestGuess = bestGuessAudioTrack(info.getMediaSource());
|
||||
|
||||
if (remoteDefault != null)
|
||||
mDefaultAudioIndex = remoteDefault;
|
||||
else if (bestGuess != null)
|
||||
mDefaultAudioIndex = bestGuess;
|
||||
Timber.d("default audio index set to %s", mDefaultAudioIndex);
|
||||
}
|
||||
|
||||
@@ -877,6 +911,10 @@ public class PlaybackController {
|
||||
Timber.d("trying to switch audio stream from %s to %s", currAudioIndex, index);
|
||||
if (currAudioIndex == index) {
|
||||
Timber.d("skipping setting audio stream, already set to requested index %s", index);
|
||||
if (mCurrentOptions.getAudioStreamIndex() == null || mCurrentOptions.getAudioStreamIndex() != index) {
|
||||
Timber.d("setting mCurrentOptions audio stream index from %s to %s", mCurrentOptions.getAudioStreamIndex(), index);
|
||||
mCurrentOptions.setAudioStreamIndex(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -886,7 +924,7 @@ public class PlaybackController {
|
||||
if (isNativeMode() && !isTranscoding() && mVideoManager.setExoPlayerTrack(index, MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams())) {
|
||||
mCurrentOptions.setMediaSourceId(getCurrentMediaSource().getId());
|
||||
mCurrentOptions.setAudioStreamIndex(index);
|
||||
} else if (!isNativeMode() && !isTranscoding() && mVideoManager.setVLCAudioTrack(index) == index) {
|
||||
} else if (!isNativeMode() && !isTranscoding() && mVideoManager.setVLCAudioTrack(index, getCurrentlyPlayingItem().getMediaStreams()) == index) {
|
||||
// if setAudioTrack succeeded it will return the requested index
|
||||
mCurrentOptions.setMediaSourceId(getCurrentMediaSource().getId());
|
||||
mCurrentOptions.setAudioStreamIndex(index);
|
||||
@@ -905,20 +943,23 @@ public class PlaybackController {
|
||||
public void switchSubtitleStream(int index) {
|
||||
if (!hasInitializedVideoManager())
|
||||
return;
|
||||
|
||||
// get current timestamp first
|
||||
refreshCurrentPosition();
|
||||
Timber.d("Setting subtitle index to: %d", index);
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
|
||||
// clear subtitles first
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
// clear the default in case there's an error loading the subtitles
|
||||
mDefaultSubIndex = -1;
|
||||
|
||||
// handle setting subtitles as disabled
|
||||
// restart playback if turning off burnt-in subtitles
|
||||
if (index < 0) {
|
||||
mCurrentOptions.setSubtitleStreamIndex(-1);
|
||||
if (burningSubs) {
|
||||
stop();
|
||||
play(mCurrentPosition, -1);
|
||||
} else {
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -929,77 +970,77 @@ public class PlaybackController {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.subtitle_error));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// handle according to delivery method
|
||||
SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index);
|
||||
if (streamInfo == null) {
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
} else {
|
||||
switch (streamInfo.getDeliveryMethod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
case Encode:
|
||||
// Gonna need to burn in so start a transcode with the sub index
|
||||
stop();
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
// handle switching on or between burnt-in subtitles, or switching to non-burnt subtitles
|
||||
// if switching from burnt-in subtitles to another type, playback still needs to be restarted
|
||||
if (burningSubs || streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
|
||||
stop();
|
||||
if (mFragment != null && streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
|
||||
play(mCurrentPosition, index);
|
||||
return;
|
||||
}
|
||||
|
||||
// when burnt-in subtitles are selected, mCurrentOptions SubtitleStreamIndex is set in startItem() as soon as playback starts
|
||||
// otherwise mCurrentOptions SubtitleStreamIndex is kept null until now so we knew subtitles needed to be enabled but weren't already
|
||||
|
||||
switch (streamInfo.getDeliveryMethod()) {
|
||||
case Embed:
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
|
||||
// error selecting internal subs
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
} else {
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
}
|
||||
play(mCurrentPosition, index);
|
||||
break;
|
||||
case Embed:
|
||||
if (!mVideoManager.isNativeMode()) {
|
||||
if (mFragment != null)
|
||||
mFragment.addManualSubtitles(null); // in case these were on
|
||||
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
|
||||
// error selecting internal subs
|
||||
if (mFragment != null)
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
}
|
||||
break;
|
||||
}
|
||||
// not using vlc - fall through to external handling
|
||||
case External:
|
||||
if (mFragment != null) mFragment.addManualSubtitles(null);
|
||||
mVideoManager.disableSubs();
|
||||
if (mFragment != null) mFragment.showSubLoadingMsg(true);
|
||||
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
|
||||
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
|
||||
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
|
||||
}
|
||||
// not using vlc - fall through to external handling
|
||||
case External:
|
||||
if (mFragment != null) mFragment.showSubLoadingMsg(true);
|
||||
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
|
||||
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
|
||||
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(final SubtitleTrackInfo info) {
|
||||
@Override
|
||||
public void onResponse(final SubtitleTrackInfo info) {
|
||||
|
||||
if (info != null) {
|
||||
Timber.d("Adding json subtitle track to player");
|
||||
if (mFragment != null) mFragment.addManualSubtitles(info);
|
||||
} else {
|
||||
Timber.e("Empty subtitle result");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.e(ex, "Error downloading subtitles");
|
||||
if (info != null) {
|
||||
Timber.d("Adding json subtitle track to player");
|
||||
if (mFragment != null) mFragment.addManualSubtitles(info);
|
||||
mCurrentOptions.setSubtitleStreamIndex(index);
|
||||
mDefaultSubIndex = index;
|
||||
} else {
|
||||
Timber.e("Empty subtitle result");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
case Hls:
|
||||
break;
|
||||
}
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.e(ex, "Error downloading subtitles");
|
||||
if (mFragment != null) {
|
||||
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
|
||||
mFragment.showSubLoadingMsg(false);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
case Hls:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
@@ -1419,7 +1460,6 @@ public class PlaybackController {
|
||||
if (mPlaybackState == PlaybackState.PAUSED) {
|
||||
mPlaybackState = PlaybackState.PLAYING;
|
||||
} else {
|
||||
|
||||
// select or disable subtitles
|
||||
Integer currentSubtitleIndex = mCurrentOptions.getSubtitleStreamIndex();
|
||||
if (mDefaultSubIndex >= 0 && currentSubtitleIndex != null && currentSubtitleIndex == mDefaultSubIndex) {
|
||||
|
||||
@@ -44,9 +44,10 @@ import org.koin.java.KoinJavaComponent;
|
||||
import org.videolan.libvlc.LibVLC;
|
||||
import org.videolan.libvlc.Media;
|
||||
import org.videolan.libvlc.interfaces.IVLCVout;
|
||||
import org.videolan.libvlc.MediaPlayer;
|
||||
import org.videolan.libvlc.MediaPlayer.TrackDescription;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
@@ -67,7 +68,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
private PlayerView mExoPlayerView;
|
||||
private AspectRatioFrameLayout mAspectRatioFrameLayout;
|
||||
private LibVLC mLibVLC;
|
||||
private org.videolan.libvlc.MediaPlayer mVlcPlayer;
|
||||
private MediaPlayer mVlcPlayer;
|
||||
private Media mCurrentMedia;
|
||||
private VlcEventHandler mVlcHandler = new VlcEventHandler();
|
||||
private Handler mHandler = new Handler();
|
||||
@@ -77,7 +78,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
private int mVideoVisibleWidth;
|
||||
private int mSarNum;
|
||||
private int mSarDen;
|
||||
private Integer exoplayerAudioIndex = null;
|
||||
|
||||
private long mForcedTime = -1;
|
||||
private long mLastTime = -1;
|
||||
@@ -149,7 +149,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
@Override
|
||||
public void onTracksInfoChanged(TracksInfo tracksInfo) {
|
||||
Timber.d("Tracks info changed");
|
||||
exoplayerAudioIndex = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -297,7 +296,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
mActivity.finish();
|
||||
return;
|
||||
}
|
||||
exoplayerAudioIndex = null;
|
||||
mExoPlayer.setPlayWhenReady(true);
|
||||
normalWidth = mExoPlayerView.getLayoutParams().width;
|
||||
normalHeight = mExoPlayerView.getLayoutParams().height;
|
||||
@@ -339,7 +337,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
|
||||
.buildUpon()
|
||||
.setTrackSelectionOverrides(overrides).build());
|
||||
exoplayerAudioIndex = null;
|
||||
} else if (mVlcPlayer != null) {
|
||||
mVlcPlayer.stop();
|
||||
}
|
||||
@@ -421,37 +418,43 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setSubtitleTrack(int index, @Nullable List<MediaStream> allStreams) {
|
||||
if (!nativeMode && allStreams != null) {
|
||||
//find the relative order of our sub index within the sub tracks in VLC
|
||||
int vlcIndex = 1; // start at 1 to account for "disabled"
|
||||
for (MediaStream stream : allStreams) {
|
||||
if (stream.getType() == MediaStreamType.Subtitle && !stream.getIsExternal()) {
|
||||
if (stream.getIndex() == index) {
|
||||
break;
|
||||
}
|
||||
vlcIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
org.videolan.libvlc.MediaPlayer.TrackDescription vlcSub;
|
||||
try {
|
||||
vlcSub = getSubtitleTracks()[vlcIndex];
|
||||
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
Timber.e("Could not locate subtitle with index %s in vlc track info", index);
|
||||
return false;
|
||||
} catch (NullPointerException e) {
|
||||
Timber.e("No subtitle tracks found in player trying to set subtitle with index %s in vlc track info", index);
|
||||
return false;
|
||||
}
|
||||
|
||||
Timber.i("Setting Vlc sub to %s", vlcSub.name);
|
||||
return mVlcPlayer.setSpuTrack(vlcSub.id);
|
||||
private int offsetStreamIndex(int index, boolean adjustByAdding, @Nullable List<MediaStream> allStreams) {
|
||||
if (index < 0 || allStreams == null)
|
||||
return -1;
|
||||
|
||||
for (MediaStream stream : allStreams) {
|
||||
if (!stream.getIsExternal())
|
||||
break;
|
||||
index += adjustByAdding ? 1 : -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
return index < 0 || index >= allStreams.size() ? -1 : index;
|
||||
}
|
||||
|
||||
public boolean setSubtitleTrack(int index, @Nullable List<MediaStream> allStreams) {
|
||||
if (isNativeMode() || allStreams == null)
|
||||
return false;
|
||||
|
||||
int vlcIndex = offsetStreamIndex(index, false, allStreams);
|
||||
|
||||
if (vlcIndex < 0)
|
||||
return false;
|
||||
|
||||
TrackDescription vlcSub = null;
|
||||
for (TrackDescription subTrack: mVlcPlayer.getSpuTracks()) {
|
||||
Timber.d("libvlc subtitle track %s %s", subTrack.id, subTrack.name);
|
||||
if (subTrack.id == vlcIndex)
|
||||
vlcSub = subTrack;
|
||||
}
|
||||
|
||||
if (vlcSub == null) {
|
||||
Timber.e("Could not locate subtitle with index %s in vlc track info", vlcIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
Timber.i("Setting Vlc sub to %s", vlcSub.name);
|
||||
return mVlcPlayer.setSpuTrack(vlcSub.id);
|
||||
|
||||
}
|
||||
|
||||
public int getExoPlayerTrack(@Nullable MediaStreamType streamType, @Nullable List<MediaStream> allStreams) {
|
||||
@@ -460,13 +463,13 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
if (streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
return -1;
|
||||
|
||||
if (exoplayerAudioIndex != null && streamType == MediaStreamType.Audio)
|
||||
return exoplayerAudioIndex;
|
||||
|
||||
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
|
||||
|
||||
int matchedIndex = -2;
|
||||
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
|
||||
for (TracksInfo.TrackGroupInfo groupInfo : exoTracks.getTrackGroupInfos()) {
|
||||
if (matchedIndex > -2)
|
||||
break;
|
||||
// Group level information.
|
||||
@C.TrackType int trackType = groupInfo.getTrackType();
|
||||
TrackGroup group = groupInfo.getTrackGroup();
|
||||
@@ -475,62 +478,50 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
Format trackFormat = group.getFormat(i);
|
||||
if (trackType == chosenTrackType) {
|
||||
if (groupInfo.isTrackSelected(i)) {
|
||||
// we found the track, set to -1 first to handle failed int parsing
|
||||
matchedIndex = -1;
|
||||
if (trackFormat.id != null) {
|
||||
int id;
|
||||
try {
|
||||
id = Integer.parseInt(trackFormat.id) - 1;
|
||||
id = Integer.parseInt(trackFormat.id);
|
||||
} catch (NumberFormatException e) {
|
||||
Timber.d("failed to parse track ID [%s]", trackFormat.id);
|
||||
return -1;
|
||||
}
|
||||
if (id >= 0 && id < allStreams.size()) {
|
||||
Timber.d("re-retrieved exoplayer track index %s", id);
|
||||
if (streamType == MediaStreamType.Audio)
|
||||
exoplayerAudioIndex = id;
|
||||
return id;
|
||||
break;
|
||||
}
|
||||
matchedIndex = id;
|
||||
}
|
||||
return -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
// offset the stream index to account for external streams
|
||||
int exoTrackID = offsetStreamIndex(matchedIndex, true, allStreams);
|
||||
if (exoTrackID < 0)
|
||||
return -1;
|
||||
|
||||
Timber.d("re-retrieved exoplayer track index %s", exoTrackID);
|
||||
return exoTrackID;
|
||||
}
|
||||
|
||||
public boolean setExoPlayerTrack(int index, @Nullable MediaStreamType streamType, @Nullable List<MediaStream> allStreams) {
|
||||
if (!nativeMode || !isInitialized() || streamType == null || allStreams == null || index < 0 || index >= allStreams.size())
|
||||
return false;
|
||||
if (streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
if (!nativeMode || !isInitialized() || allStreams == null || streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
return false;
|
||||
|
||||
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
|
||||
|
||||
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
|
||||
|
||||
List<String> internallyRenderedSubFormats = Arrays.asList("ass", "ssa");
|
||||
|
||||
// make sure the chosen track exists, is the correct type, and isn't external
|
||||
boolean isValidMediaStream = false;
|
||||
for (MediaStream stream : allStreams) {
|
||||
Timber.d("MediaStream track %s type %s label %s codec %s isExternal %s", stream.getIndex(), stream.getType(), stream.getTitle(), stream.getCodec(), stream.getIsExternal());
|
||||
if (stream.getType() == streamType) {
|
||||
if (stream.getIndex() == index) {
|
||||
if (stream.getIsExternal())
|
||||
continue;
|
||||
if (streamType == MediaStreamType.Subtitle && !internallyRenderedSubFormats.contains(stream.getCodec())) {
|
||||
continue;
|
||||
}
|
||||
isValidMediaStream = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidMediaStream)
|
||||
MediaStream candidate = allStreams.get(index);
|
||||
if (candidate.getIsExternal() || candidate.getType() != streamType)
|
||||
return false;
|
||||
|
||||
// MediaStream IDs start at 0 and exoplayer tracks start at 1
|
||||
// force selection of the chosen track by setting its TrackGroup as the only allowed group for its type
|
||||
int exoTrackID = offsetStreamIndex(index, false, allStreams);
|
||||
if (exoTrackID < 0)
|
||||
return false;
|
||||
|
||||
// print the streams for debugging
|
||||
for (MediaStream stream : allStreams) {
|
||||
Timber.d("MediaStream track %s type %s label %s codec %s isExternal %s", stream.getIndex(), stream.getType(), stream.getTitle(), stream.getCodec(), stream.getIsExternal());
|
||||
}
|
||||
|
||||
// design choices for exoplayer track selection overrides:
|
||||
// * build upon the prior parameters so we can mix overrides of different track types without erasing priors
|
||||
@@ -539,8 +530,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
// if we want most formats to be handled by the external subtitle handler (which has adjustable size, background), we leave sub track selection disabled
|
||||
// if we decide to use exoplayer to render a specific subtitle format, allow subtitle track selection and restrict selection to the chosen group
|
||||
|
||||
int exoTrackID = index + 1;
|
||||
|
||||
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
|
||||
TrackGroup matchedGroup = null;
|
||||
for (TracksInfo.TrackGroupInfo groupInfo : exoTracks.getTrackGroupInfos()) {
|
||||
// Group level information.
|
||||
@@ -597,63 +587,51 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getVLCAudioTrack() {
|
||||
public int getVLCAudioTrack(@Nullable List<MediaStream> allStreams) {
|
||||
if (!isInitialized() || nativeMode)
|
||||
return -1;
|
||||
return mVlcPlayer.getAudioTrack();
|
||||
|
||||
int ndx = offsetStreamIndex(mVlcPlayer.getAudioTrack(), true, allStreams);
|
||||
Timber.d("re-retrieved libVLC audio track index %s", ndx);
|
||||
|
||||
return ndx;
|
||||
}
|
||||
|
||||
public int setVLCAudioTrack(int ndx) {
|
||||
if (!isInitialized() || ndx < 0)
|
||||
public int setVLCAudioTrack(int ndx, @Nullable List<MediaStream> allStreams) {
|
||||
if (!isInitialized() || isNativeMode())
|
||||
return -1;
|
||||
|
||||
if (isNativeMode()) {
|
||||
Timber.e("Cannot set audio track in native mode");
|
||||
int vlcID = offsetStreamIndex(ndx, false, allStreams);
|
||||
if (vlcID < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
//debug
|
||||
Timber.d("Setting VLC audio track index to: %d", ndx);
|
||||
for (org.videolan.libvlc.MediaPlayer.TrackDescription track : mVlcPlayer.getAudioTracks()) {
|
||||
TrackDescription vlcTrack = null;
|
||||
Timber.d("Setting VLC audio track index to: %d", vlcID);
|
||||
|
||||
int vlcNdx = 0;
|
||||
for (TrackDescription track : mVlcPlayer.getAudioTracks()) {
|
||||
Timber.d("VLC Audio Track: %s / %d", track.name, track.id);
|
||||
}
|
||||
//
|
||||
|
||||
boolean matched = false;
|
||||
for (org.videolan.libvlc.MediaPlayer.TrackDescription track : mVlcPlayer.getAudioTracks()) {
|
||||
if (track.id == ndx) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
if (track.id == vlcID)
|
||||
vlcTrack = track;
|
||||
if (vlcTrack == null)
|
||||
vlcNdx++;
|
||||
}
|
||||
|
||||
org.videolan.libvlc.MediaPlayer.TrackDescription vlcTrack = null;
|
||||
|
||||
if (matched) {
|
||||
try {
|
||||
vlcTrack = mVlcPlayer.getAudioTracks()[ndx];
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
Timber.e("Could not locate audio with index %s in vlc track info", ndx);
|
||||
return -1;
|
||||
} catch (NullPointerException e) {
|
||||
Timber.e("Could not locate audio track with index %s in vlc, null exception", ndx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched || vlcTrack == null) {
|
||||
Timber.e("Could not locate audio track with index %s in vlc", ndx);
|
||||
if (vlcTrack == null) {
|
||||
Timber.e("Could not locate audio track with index %s in vlc", vlcID);
|
||||
return -1;
|
||||
} else if (mVlcPlayer.getAudioTrack() == ndx) {
|
||||
}
|
||||
|
||||
if (mVlcPlayer.getAudioTrack() == vlcID) {
|
||||
Timber.d("provided index points to the audio track already in use, aborting");
|
||||
return -1;
|
||||
return ndx;
|
||||
}
|
||||
|
||||
if (mVlcPlayer.setAudioTrack(vlcTrack.id)) {
|
||||
Timber.i("Setting by ID was successful");
|
||||
} else {
|
||||
Timber.i("Setting by ID not successful, trying index");
|
||||
mVlcPlayer.setAudioTrack(ndx);
|
||||
Timber.i("Setting by ID not successful, trying index %s", vlcNdx);
|
||||
mVlcPlayer.setAudioTrack(vlcNdx);
|
||||
}
|
||||
return ndx;
|
||||
}
|
||||
@@ -729,10 +707,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
}
|
||||
}
|
||||
|
||||
public org.videolan.libvlc.MediaPlayer.TrackDescription[] getSubtitleTracks() {
|
||||
return nativeMode ? null : mVlcPlayer.getSpuTracks();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
stopPlayback();
|
||||
releasePlayer();
|
||||
@@ -746,7 +720,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
// Create a new media player
|
||||
ArrayList<String> options = new ArrayList<>(20);
|
||||
options.add("--network-caching=" + buffer);
|
||||
options.add("--no-audio-time-stretch");
|
||||
options.add("--audio-time-stretch");
|
||||
options.add("--avcodec-skiploopfilter");
|
||||
options.add("1");
|
||||
options.add("--avcodec-skip-frame");
|
||||
@@ -772,7 +746,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
mLibVLC = new LibVLC(mActivity, options);
|
||||
Timber.i("Network buffer set to %d", buffer);
|
||||
|
||||
mVlcPlayer = new org.videolan.libvlc.MediaPlayer(mLibVLC);
|
||||
mVlcPlayer = new MediaPlayer(mLibVLC);
|
||||
setVlcAudioOptions();
|
||||
|
||||
mSurfaceHolder.addCallback(mSurfaceCallback);
|
||||
|
||||
@@ -13,13 +13,10 @@ class ButtonRemapPreference(
|
||||
) : DialogPreference(context, attrs) {
|
||||
override fun getDialogLayoutResource() = R.layout.preference_button_remap
|
||||
|
||||
private var innerKeyCode: Int = -1
|
||||
var keyCode: Int
|
||||
get() = innerKeyCode
|
||||
var keyCode: Int = -1
|
||||
set(value) {
|
||||
innerKeyCode = value
|
||||
notifyDependencyChange(false)
|
||||
notifyChanged()
|
||||
field = value
|
||||
callChangeListener(value)
|
||||
}
|
||||
var defaultKeyCode: Int = -1
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.jellyfin.androidtv.preference.constant.AudioBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.NEXTUP_TIMER_DISABLED
|
||||
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
|
||||
import org.jellyfin.androidtv.ui.preference.custom.DurationSeekBarPreference
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
|
||||
@@ -134,10 +135,9 @@ class PlaybackPreferencesScreen : OptionsFragment() {
|
||||
depends { userPreferences[UserPreferences.videoPlayer] == PreferredVideoPlayer.EXTERNAL }
|
||||
}
|
||||
|
||||
checkbox {
|
||||
enum<RefreshRateSwitchingBehavior> {
|
||||
setTitle(R.string.lbl_refresh_switching)
|
||||
setContent(R.string.pref_refresh_switching_description)
|
||||
bind(userPreferences, UserPreferences.refreshRateSwitchingEnabled)
|
||||
bind(userPreferences, UserPreferences.refreshRateSwitchingBehavior)
|
||||
depends { DeviceUtils.is60() && userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import java.util.HashMap;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class CardPresenter extends Presenter {
|
||||
private static final double ASPECT_RATIO_BANNER = 1000 / 185;
|
||||
private static final double ASPECT_RATIO_BANNER = 1000.0 / 185.0;
|
||||
|
||||
private int mStaticHeight = 300;
|
||||
private ImageType mImageType = ImageType.DEFAULT;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<string name="settings_title">Preferences</string>
|
||||
<string name="pref_authentication_cat">Authentication</string>
|
||||
<string name="chapters">Chapters</string>
|
||||
<string name="state_disabled">Disabled</string>
|
||||
<string name="lbl_settings">Preferences</string>
|
||||
<string name="lbl_continue_watching">Continue Watching</string>
|
||||
<string name="lbl_next_up">Next Up</string>
|
||||
@@ -147,7 +148,7 @@
|
||||
<string name="lbl_filters">Filters</string>
|
||||
<string name="msg_subtitles_loading">Subtitles loading…</string>
|
||||
<string name="msg_unable_load_subs">Unable to load subtitle</string>
|
||||
<string name="msg_burn_sub_warning">Burned in subs can take a few minutes. Please be patient…</string>
|
||||
<string name="msg_burn_sub_warning">Burning in subtitles. This may take a few seconds…</string>
|
||||
<string name="lbl_recording">Recording</string>
|
||||
<string name="lbl_no_recordings">No Recordings Available</string>
|
||||
<string name="lbl_live_tv_guide">Live TV Guide</string>
|
||||
@@ -204,8 +205,6 @@
|
||||
<string name="lbl_open">Open</string>
|
||||
<string name="lbl_show_backdrop">Show backdrops</string>
|
||||
<string name="pref_show_backdrop_description">Change background image to selected items</string>
|
||||
<string name="lbl_refresh_switching">Refresh rate switching</string>
|
||||
<string name="pref_refresh_switching_description">Monitor refresh rate is changed to match video</string>
|
||||
<string name="lbl_show_premieres">Show season premieres</string>
|
||||
<string name="desc_premieres">Show a row of new episode pilots for any series you watch</string>
|
||||
<string name="lbl_bitstream_dts">Bitstream DTS audio</string>
|
||||
@@ -486,4 +485,17 @@
|
||||
<string name="pref_direct_stream_live_off">Transcoding is used when necessary</string>
|
||||
<string name="pref_direct_stream_live_on">Transcoding is disabled</string>
|
||||
<string name="home_section_resume_book">Continue reading</string>
|
||||
<string name="action_login">Sign in</string>
|
||||
<string name="action_use_password">Use a password</string>
|
||||
<string name="action_use_quickconnect">Use QuickConnect</string>
|
||||
<string name="input_username">Username</string>
|
||||
<string name="input_password">Password</string>
|
||||
<string name="login_connect_to">Connecting to %1$s</string>
|
||||
<string name="login_other_options">Other options:</string>
|
||||
<string name="login_quickconnect_step_1">1. Open the Jellyfin app on your phone or webbrowser, and sign in with your account</string>
|
||||
<string name="login_quickconnect_step_2">2. Open the user menu and go to the Quick Connect page</string>
|
||||
<string name="login_quickconnect_step_3">3. Enter the following code:</string>
|
||||
<string name="lbl_refresh_switching">Refresh rate switching</string>
|
||||
<string name="pref_refresh_rate_scale_on_tv">Scale on TV</string>
|
||||
<string name="pref_refresh_rate_scale_on_device">Scale on device</string>
|
||||
</resources>
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user