diff --git a/app/src/main/java/org/jellyfin/androidtv/constant/Codec.kt b/app/src/main/java/org/jellyfin/androidtv/constant/Codec.kt index 100a8a6e9c..82f185f67c 100644 --- a/app/src/main/java/org/jellyfin/androidtv/constant/Codec.kt +++ b/app/src/main/java/org/jellyfin/androidtv/constant/Codec.kt @@ -76,5 +76,8 @@ object Codec { const val SUB = "sub" const val SUBRIP = "subrip" const val VTT = "vtt" + const val SMIL = "smil" + const val TTML = "ttml" + const val WEBVTT = "webvtt" } } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java index d388433a78..29cf564cd4 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java @@ -18,6 +18,7 @@ import org.jellyfin.androidtv.auth.repository.UserRepository; import org.jellyfin.androidtv.data.compat.PlaybackException; import org.jellyfin.androidtv.data.compat.StreamInfo; +import org.jellyfin.androidtv.data.compat.SubtitleStreamInfo; import org.jellyfin.androidtv.data.compat.VideoOptions; import org.jellyfin.androidtv.data.service.BackgroundService; import org.jellyfin.androidtv.preference.UserPreferences; @@ -31,6 +32,7 @@ import org.jellyfin.androidtv.util.sdk.compat.JavaCompat; import org.jellyfin.apiclient.interaction.ApiClient; import org.jellyfin.apiclient.interaction.Response; +import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod; import org.jellyfin.apiclient.model.dto.UserItemDataDto; import org.jellyfin.apiclient.model.session.PlayMethod; import org.jellyfin.sdk.model.api.BaseItemKind; @@ -39,6 +41,7 @@ import java.io.File; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import kotlin.Lazy; import timber.log.Timber; @@ -70,12 +73,17 @@ public class ExternalPlayerActivity extends FragmentActivity { static final String API_MX_TITLE = "title"; static final String API_MX_SEEK_POSITION = "position"; static final String API_MX_FILENAME = "filename"; + static final String API_MX_SECURE_URI = "secure_uri"; static final String API_MX_RETURN_RESULT = "return_result"; static final String API_MX_RESULT_ID = "com.mxtech.intent.result.VIEW"; static final String API_MX_RESULT_POSITION = "position"; static final String API_MX_RESULT_END_BY = "end_by"; static final String API_MX_RESULT_END_BY_USER = "user"; static final String API_MX_RESULT_END_BY_PLAYBACK_COMPLETION = "playback_completion"; + static final String API_MX_SUBS = "subs"; + static final String API_MX_SUBS_NAME = "subs.name"; + static final String API_MX_SUBS_FILENAME = "subs.filename"; + static final String API_MX_SUBS_ENABLE = "subs.enable"; // https://wiki.videolan.org/Android_Player_Intents/ static final String API_VLC_TITLE = "title"; @@ -83,6 +91,7 @@ public class ExternalPlayerActivity extends FragmentActivity { static final String API_VLC_FROM_START = "from_start"; static final String API_VLC_RESULT_ID = "org.videolan.vlc.player.result"; static final String API_VLC_RESULT_POSITION = "extra_position"; + static final String API_VLC_SUBS_ENABLE = "subtitles_location"; // https://www.vimu.tv/player-api static final String API_VIMU_TITLE = "forcename"; @@ -401,6 +410,10 @@ protected void startExternalActivity(String path, String container) { external.putExtra(API_MX_FILENAME, file.getName()); } } + + external.putExtra(API_MX_SECURE_URI, true); + this.adaptExternalSubtitles(mCurrentStreamInfo, external); + //End player API params Timber.i("Starting external playback of path: %s and mime: video/%s at position/ms: %s",path,container,mPosition); @@ -416,4 +429,50 @@ protected void startExternalActivity(String path, String container) { handlePlayerError(); } } + + /** + * Adapt external subtitles for external players. (e.g., MX Player, MPV, VLC, nPlayer) + * External subtitles have higher priority than embedded subtitles. + * + * @param mediaStreamInfo Current media stream info used to get subtitle profiles. + * @param playerIntent Put player API params of sub urls. + */ + private void adaptExternalSubtitles(StreamInfo mediaStreamInfo, Intent playerIntent) { + + List externalSubs = mediaStreamInfo.GetSubtitleProfiles(false, + apiClient.getValue().getApiUrl(), apiClient.getValue().getAccessToken()).stream() + .filter(stream -> stream.getDeliveryMethod() == SubtitleDeliveryMethod.External && stream.getUrl() != null) + .collect(Collectors.toList()); + + Uri[] subUrls = externalSubs.stream().map(stream -> Uri.parse(stream.getUrl())).toArray(Uri[]::new); + String[] subNames = externalSubs.stream().map(SubtitleStreamInfo::getDisplayTitle).toArray(String[]::new); + String[] subLanguages = externalSubs.stream().map(SubtitleStreamInfo::getName).toArray(String[]::new); + + // select subtitle + Integer selectedSubStreamIndex = mediaStreamInfo.getMediaSource().getDefaultSubtitleStreamIndex(); + Uri selectedSubUrl = null; + if (selectedSubStreamIndex != null) { + selectedSubUrl = externalSubs.stream() + .filter(stream -> stream.getIndex() == selectedSubStreamIndex) + .map(stream -> Uri.parse(stream.getUrl())) + .findFirst() + .orElse(null); + } + if (selectedSubUrl == null && subUrls.length > 0) { + selectedSubUrl = subUrls[0]; + } + + // MX Player API / MPV + playerIntent.putExtra(API_MX_SUBS, subUrls); + playerIntent.putExtra(API_MX_SUBS_NAME, subNames); + playerIntent.putExtra(API_MX_SUBS_FILENAME, subLanguages); + if (selectedSubUrl != null) { + playerIntent.putExtra(API_MX_SUBS_ENABLE, new Uri[] {selectedSubUrl}); + } + + // VLC + if (selectedSubUrl != null) { + playerIntent.putExtra(API_VLC_SUBS_ENABLE, selectedSubUrl); + } + } } diff --git a/app/src/main/java/org/jellyfin/androidtv/util/profile/ExternalPlayerProfile.kt b/app/src/main/java/org/jellyfin/androidtv/util/profile/ExternalPlayerProfile.kt index 186375d251..a56e277f80 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/profile/ExternalPlayerProfile.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/profile/ExternalPlayerProfile.kt @@ -18,51 +18,9 @@ class ExternalPlayerProfile : DeviceProfile() { directPlayProfiles = arrayOf( DirectPlayProfile().apply { type = DlnaProfileType.Video - container = arrayOf( - Codec.Container.M4V, - Codec.Container.`3GP`, - Codec.Container.TS, - Codec.Container.MPEGTS, - Codec.Container.MOV, - Codec.Container.XVID, - Codec.Container.VOB, - Codec.Container.MKV, - Codec.Container.WMV, - Codec.Container.ASF, - Codec.Container.OGM, - Codec.Container.OGV, - Codec.Container.M2V, - Codec.Container.AVI, - Codec.Container.MPG, - Codec.Container.MPEG, - Codec.Container.MP4, - Codec.Container.WEBM, - Codec.Container.DVR_MS, - Codec.Container.WTV - ).joinToString(",") - } - ) - - transcodingProfiles = arrayOf( - // MKV video profile - TranscodingProfile().apply { - type = DlnaProfileType.Video - context = EncodingContext.Streaming - container = Codec.Container.MKV - videoCodec = Codec.Video.H264 - audioCodec = arrayOf( - Codec.Audio.AAC, - Codec.Audio.MP3, - Codec.Audio.AC3 - ).joinToString(",") - copyTimestamps = true }, - // MP3 audio profile - TranscodingProfile().apply { + DirectPlayProfile().apply { type = DlnaProfileType.Audio - context = EncodingContext.Streaming - container = Codec.Container.MP3 - audioCodec = Codec.Audio.MP3 } ) @@ -77,7 +35,25 @@ class ExternalPlayerProfile : DeviceProfile() { subtitleProfile(Codec.Subtitle.VTT, SubtitleDeliveryMethod.Embed), subtitleProfile(Codec.Subtitle.SUB, SubtitleDeliveryMethod.Embed), subtitleProfile(Codec.Subtitle.IDX, SubtitleDeliveryMethod.Embed), - subtitleProfile(Codec.Subtitle.SMI, SubtitleDeliveryMethod.Embed) + subtitleProfile(Codec.Subtitle.SMI, SubtitleDeliveryMethod.Embed), + subtitleProfile(Codec.Subtitle.SMIL, SubtitleDeliveryMethod.Embed), + subtitleProfile(Codec.Subtitle.TTML, SubtitleDeliveryMethod.Embed), + subtitleProfile(Codec.Subtitle.WEBVTT, SubtitleDeliveryMethod.Embed), + + subtitleProfile(Codec.Subtitle.SRT, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.SUBRIP, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.ASS, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.SSA, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.PGS, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.PGSSUB, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.DVDSUB, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.VTT, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.SUB, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.IDX, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.SMI, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.SMIL, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.TTML, SubtitleDeliveryMethod.External), + subtitleProfile(Codec.Subtitle.WEBVTT, SubtitleDeliveryMethod.External) ) } }