feat: add language selector to audio player
This commit is contained in:
parent
77649d388c
commit
366c39d4c6
|
@ -13,6 +13,7 @@ import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
|
@ -27,11 +28,13 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
|
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
@ -44,6 +47,10 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class PlayQueueActivity extends AppCompatActivity
|
public final class PlayQueueActivity extends AppCompatActivity
|
||||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||||
|
@ -52,6 +59,8 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
|
private static final int MENU_ID_AUDIO_TRACK = 71;
|
||||||
|
|
||||||
private Player player;
|
private Player player;
|
||||||
|
|
||||||
private boolean serviceBound;
|
private boolean serviceBound;
|
||||||
|
@ -97,6 +106,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
this.menu = m;
|
this.menu = m;
|
||||||
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
||||||
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
||||||
|
buildAudioTrackMenu();
|
||||||
onMaybeMuteChanged();
|
onMaybeMuteChanged();
|
||||||
// to avoid null reference
|
// to avoid null reference
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
@ -153,6 +163,12 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
|
||||||
|
onAudioTrackClick(item.getItemId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -591,4 +607,77 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
item.setIcon(player.isMuted() ? R.drawable.ic_volume_off : R.drawable.ic_volume_up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioTrackUpdate() {
|
||||||
|
buildAudioTrackMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildAudioTrackMenu() {
|
||||||
|
if (menu == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
|
||||||
|
final List<AudioStream> availableStreams =
|
||||||
|
Optional.ofNullable(player.getCurrentMetadata())
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (availableStreams == null || availableStreams.size() < 2) {
|
||||||
|
audioTrackSelector.setVisible(false);
|
||||||
|
} else {
|
||||||
|
final SubMenu audioTrackMenu = audioTrackSelector.getSubMenu();
|
||||||
|
audioTrackMenu.clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < availableStreams.size(); i++) {
|
||||||
|
final AudioStream audioStream = availableStreams.get(i);
|
||||||
|
if (audioStream.getAudioTrackName() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTrackMenu.add(MENU_ID_AUDIO_TRACK, i, Menu.NONE,
|
||||||
|
audioStream.getAudioTrackName());
|
||||||
|
}
|
||||||
|
|
||||||
|
player.getSelectedAudioStream().ifPresent(s -> {
|
||||||
|
final String trackName = Objects.toString(s.getAudioTrackName(), "");
|
||||||
|
audioTrackSelector.setTitle(getString(R.string.play_queue_audio_track) + trackName);
|
||||||
|
|
||||||
|
final String shortName = s.getAudioLocale() != null
|
||||||
|
? s.getAudioLocale().getLanguage() : trackName;
|
||||||
|
audioTrackSelector.setTitleCondensed(
|
||||||
|
shortName.substring(0, Math.min(shortName.length(), 2)));
|
||||||
|
audioTrackSelector.setVisible(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an item from the audio track selector is selected.
|
||||||
|
*
|
||||||
|
* @param itemId index of the selected item
|
||||||
|
*/
|
||||||
|
private void onAudioTrackClick(final int itemId) {
|
||||||
|
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||||
|
if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaItemTag.AudioTrack audioTrack =
|
||||||
|
currentMetadata.getMaybeAudioTrack().get();
|
||||||
|
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
|
||||||
|
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
|
||||||
|
if (selectedStreamIndex == itemId || availableStreams.size() <= itemId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.saveStreamProgressState();
|
||||||
|
final String newAudioTrack = availableStreams.get(itemId).getAudioTrackId();
|
||||||
|
player.setRecovery();
|
||||||
|
player.setAudioTrack(newAudioTrack);
|
||||||
|
player.reloadPlayQueueManager();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1243,6 +1243,9 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
}
|
}
|
||||||
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
||||||
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
||||||
|
final MediaItemTag.AudioTrack previousAudioTrack =
|
||||||
|
Optional.ofNullable(currentMetadata)
|
||||||
|
.flatMap(MediaItemTag::getMaybeAudioTrack).orElse(null);
|
||||||
currentMetadata = tag;
|
currentMetadata = tag;
|
||||||
|
|
||||||
if (!currentMetadata.getErrors().isEmpty()) {
|
if (!currentMetadata.getErrors().isEmpty()) {
|
||||||
|
@ -1263,6 +1266,12 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
||||||
// only update with the new stream info if it has actually changed
|
// only update with the new stream info if it has actually changed
|
||||||
updateMetadataWith(info);
|
updateMetadataWith(info);
|
||||||
|
} else if (previousAudioTrack == null
|
||||||
|
|| tag.getMaybeAudioTrack()
|
||||||
|
.map(t -> t.getSelectedAudioStreamIndex()
|
||||||
|
!= previousAudioTrack.getSelectedAudioStreamIndex())
|
||||||
|
.orElse(false)) {
|
||||||
|
notifyAudioTrackUpdateToListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1759,6 +1768,7 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
registerStreamViewed();
|
registerStreamViewed();
|
||||||
|
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
notifyAudioTrackUpdateToListeners();
|
||||||
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
UIs.call(playerUi -> playerUi.onMetadataChanged(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1890,8 +1900,8 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
|
|
||||||
public Optional<AudioStream> getSelectedAudioStream() {
|
public Optional<AudioStream> getSelectedAudioStream() {
|
||||||
return Optional.ofNullable(currentMetadata)
|
return Optional.ofNullable(currentMetadata)
|
||||||
.flatMap(MediaItemTag::getMaybeAudioLanguage)
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
.map(MediaItemTag.AudioLanguage::getSelectedAudioStream);
|
.map(MediaItemTag.AudioTrack::getSelectedAudioStream);
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -2024,6 +2034,15 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyAudioTrackUpdateToListeners() {
|
||||||
|
if (fragmentListener != null) {
|
||||||
|
fragmentListener.onAudioTrackUpdate();
|
||||||
|
}
|
||||||
|
if (activityListener != null) {
|
||||||
|
activityListener.onAudioTrackUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void useVideoSource(final boolean videoEnabled) {
|
public void useVideoSource(final boolean videoEnabled) {
|
||||||
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
|
@ -2185,8 +2204,9 @@ public final class Player implements PlaybackListener, Listener {
|
||||||
videoResolver.setPlaybackQuality(quality);
|
videoResolver.setPlaybackQuality(quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioLanguage(@Nullable final String language) {
|
public void setAudioTrack(@Nullable final String audioTrackId) {
|
||||||
videoResolver.setAudioLanguage(language);
|
videoResolver.setAudioTrack(audioTrackId);
|
||||||
|
audioResolver.setAudioTrack(audioTrackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,5 +11,6 @@ public interface PlayerEventListener {
|
||||||
PlaybackParameters parameters);
|
PlaybackParameters parameters);
|
||||||
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
||||||
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
|
void onMetadataUpdate(StreamInfo info, PlayQueue queue);
|
||||||
|
default void onAudioTrackUpdate() { }
|
||||||
void onServiceStopped();
|
void onServiceStopped();
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ public interface MediaItemTag {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
default Optional<AudioLanguage> getMaybeAudioLanguage() {
|
default Optional<AudioTrack> getMaybeAudioTrack() {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,20 +135,20 @@ public interface MediaItemTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AudioLanguage {
|
final class AudioTrack {
|
||||||
@NonNull
|
@NonNull
|
||||||
private final List<AudioStream> audioStreams;
|
private final List<AudioStream> audioStreams;
|
||||||
private final int selectedAudioStreamIndex;
|
private final int selectedAudioStreamIndex;
|
||||||
|
|
||||||
private AudioLanguage(@NonNull final List<AudioStream> audioStreams,
|
private AudioTrack(@NonNull final List<AudioStream> audioStreams,
|
||||||
final int selectedAudioStreamIndex) {
|
final int selectedAudioStreamIndex) {
|
||||||
this.audioStreams = audioStreams;
|
this.audioStreams = audioStreams;
|
||||||
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
|
this.selectedAudioStreamIndex = selectedAudioStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
static AudioLanguage of(@NonNull final List<AudioStream> audioStreams,
|
static AudioTrack of(@NonNull final List<AudioStream> audioStreams,
|
||||||
final int selectedAudioStreamIndex) {
|
final int selectedAudioStreamIndex) {
|
||||||
return new AudioLanguage(audioStreams, selectedAudioStreamIndex);
|
return new AudioTrack(audioStreams, selectedAudioStreamIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
|
@ -26,17 +26,17 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
@Nullable
|
@Nullable
|
||||||
private final MediaItemTag.Quality quality;
|
private final MediaItemTag.Quality quality;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final MediaItemTag.AudioLanguage audioLanguage;
|
private final MediaItemTag.AudioTrack audioTrack;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final Object extras;
|
private final Object extras;
|
||||||
|
|
||||||
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
private StreamInfoTag(@NonNull final StreamInfo streamInfo,
|
||||||
@Nullable final MediaItemTag.Quality quality,
|
@Nullable final MediaItemTag.Quality quality,
|
||||||
@Nullable final MediaItemTag.AudioLanguage audioLanguage,
|
@Nullable final MediaItemTag.AudioTrack audioTrack,
|
||||||
@Nullable final Object extras) {
|
@Nullable final Object extras) {
|
||||||
this.streamInfo = streamInfo;
|
this.streamInfo = streamInfo;
|
||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
this.audioLanguage = audioLanguage;
|
this.audioTrack = audioTrack;
|
||||||
this.extras = extras;
|
this.extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +46,17 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
@NonNull final List<AudioStream> audioStreams,
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
final int selectedAudioStreamIndex) {
|
final int selectedAudioStreamIndex) {
|
||||||
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
final Quality quality = Quality.of(sortedVideoStreams, selectedVideoStreamIndex);
|
||||||
final AudioLanguage audioLanguage =
|
final AudioTrack audioTrack =
|
||||||
AudioLanguage.of(audioStreams, selectedAudioStreamIndex);
|
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
||||||
return new StreamInfoTag(streamInfo, quality, audioLanguage, null);
|
return new StreamInfoTag(streamInfo, quality, audioTrack, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo,
|
||||||
|
@NonNull final List<AudioStream> audioStreams,
|
||||||
|
final int selectedAudioStreamIndex) {
|
||||||
|
final AudioTrack audioTrack =
|
||||||
|
AudioTrack.of(audioStreams, selectedAudioStreamIndex);
|
||||||
|
return new StreamInfoTag(streamInfo, null, audioTrack, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
public static StreamInfoTag of(@NonNull final StreamInfo streamInfo) {
|
||||||
|
@ -114,8 +122,8 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Optional<AudioLanguage> getMaybeAudioLanguage() {
|
public Optional<AudioTrack> getMaybeAudioTrack() {
|
||||||
return Optional.ofNullable(audioLanguage);
|
return Optional.ofNullable(audioTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -125,6 +133,6 @@ public final class StreamInfoTag implements MediaItemTag {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
public StreamInfoTag withExtras(@NonNull final Object extra) {
|
||||||
return new StreamInfoTag(streamInfo, quality, audioLanguage, extra);
|
return new StreamInfoTag(streamInfo, quality, audioTrack, extra);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.player.resolver;
|
package org.schabi.newpipe.player.resolver;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getFilteredAudioStreams;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
|
import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -28,6 +29,8 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final PlayerDataSource dataSource;
|
private final PlayerDataSource dataSource;
|
||||||
|
@Nullable
|
||||||
|
private String audioTrack;
|
||||||
|
|
||||||
public AudioPlaybackResolver(@NonNull final Context context,
|
public AudioPlaybackResolver(@NonNull final Context context,
|
||||||
@NonNull final PlayerDataSource dataSource) {
|
@NonNull final PlayerDataSource dataSource) {
|
||||||
|
@ -43,12 +46,36 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
return liveSource;
|
return liveSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Stream stream = getAudioSource(info);
|
final List<AudioStream> audioStreams =
|
||||||
if (stream == null) {
|
getFilteredAudioStreams(context, info.getAudioStreams());
|
||||||
return null;
|
final Stream stream;
|
||||||
}
|
final MediaItemTag tag;
|
||||||
|
|
||||||
final MediaItemTag tag = StreamInfoTag.of(info);
|
if (!audioStreams.isEmpty()) {
|
||||||
|
int audioIndex = 0;
|
||||||
|
|
||||||
|
if (audioTrack != null) {
|
||||||
|
for (int i = 0; i < audioStreams.size(); i++) {
|
||||||
|
final AudioStream audioStream = audioStreams.get(i);
|
||||||
|
if (audioStream.getAudioTrackId() != null
|
||||||
|
&& audioStream.getAudioTrackId().equals(audioTrack)) {
|
||||||
|
audioIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream = getStreamForIndex(audioIndex, audioStreams);
|
||||||
|
tag = StreamInfoTag.of(info, audioStreams, audioIndex);
|
||||||
|
} else {
|
||||||
|
final List<VideoStream> videoStreams = getNonTorrentStreams(info.getVideoStreams());
|
||||||
|
if (!videoStreams.isEmpty()) {
|
||||||
|
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
||||||
|
stream = getStreamForIndex(index, videoStreams);
|
||||||
|
tag = StreamInfoTag.of(info);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return PlaybackResolver.buildMediaSource(
|
return PlaybackResolver.buildMediaSource(
|
||||||
|
@ -59,29 +86,6 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a stream to be played as audio. If a service has no separate {@link AudioStream}s we
|
|
||||||
* use a video stream as audio source to support audio background playback.
|
|
||||||
*
|
|
||||||
* @param info of the stream
|
|
||||||
* @return the audio source to use or null if none could be found
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private Stream getAudioSource(@NonNull final StreamInfo info) {
|
|
||||||
final List<AudioStream> audioStreams = getNonTorrentStreams(info.getAudioStreams());
|
|
||||||
if (!audioStreams.isEmpty()) {
|
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
|
|
||||||
return getStreamForIndex(index, audioStreams);
|
|
||||||
} else {
|
|
||||||
final List<VideoStream> videoStreams = getNonTorrentStreams(info.getVideoStreams());
|
|
||||||
if (!videoStreams.isEmpty()) {
|
|
||||||
final int index = ListHelper.getDefaultResolutionIndex(context, videoStreams);
|
|
||||||
return getStreamForIndex(index, videoStreams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
Stream getStreamForIndex(final int index, @NonNull final List<? extends Stream> streams) {
|
||||||
if (index >= 0 && index < streams.size()) {
|
if (index >= 0 && index < streams.size()) {
|
||||||
|
@ -89,4 +93,13 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getAudioTrack() {
|
||||||
|
return audioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||||
|
this.audioTrack = audioLanguage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
@Nullable
|
@Nullable
|
||||||
private String playbackQuality;
|
private String playbackQuality;
|
||||||
@Nullable
|
@Nullable
|
||||||
private String audioLanguage;
|
private String audioTrack;
|
||||||
|
|
||||||
public enum SourceType {
|
public enum SourceType {
|
||||||
LIVE_STREAM,
|
LIVE_STREAM,
|
||||||
|
@ -91,11 +91,11 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
int audioIndex = 0;
|
int audioIndex = 0;
|
||||||
if (audioLanguage != null) {
|
if (audioTrack != null) {
|
||||||
for (int i = 0; i < audioStreamsList.size(); i++) {
|
for (int i = 0; i < audioStreamsList.size(); i++) {
|
||||||
final AudioStream stream = audioStreamsList.get(i);
|
final AudioStream stream = audioStreamsList.get(i);
|
||||||
if (stream.getAudioTrackId() != null
|
if (stream.getAudioTrackId() != null
|
||||||
&& stream.getAudioTrackId().equals(audioLanguage)) {
|
&& stream.getAudioTrackId().equals(audioTrack)) {
|
||||||
audioIndex = i;
|
audioIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -107,8 +107,8 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
@Nullable final VideoStream video = tag.getMaybeQuality()
|
@Nullable final VideoStream video = tag.getMaybeQuality()
|
||||||
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
.map(MediaItemTag.Quality::getSelectedVideoStream)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
@Nullable final AudioStream audio = tag.getMaybeAudioLanguage()
|
@Nullable final AudioStream audio = tag.getMaybeAudioTrack()
|
||||||
.map(MediaItemTag.AudioLanguage::getSelectedAudioStream)
|
.map(MediaItemTag.AudioTrack::getSelectedAudioStream)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
if (video != null) {
|
if (video != null) {
|
||||||
|
@ -124,7 +124,7 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
|
|
||||||
// Use the audio stream if there is no video stream, or
|
// Use the audio stream if there is no video stream, or
|
||||||
// merge with audio stream in case if video does not contain audio
|
// merge with audio stream in case if video does not contain audio
|
||||||
if (audio != null && (video == null || video.isVideoOnly() || audioLanguage != null)) {
|
if (audio != null && (video == null || video.isVideoOnly() || audioTrack != null)) {
|
||||||
try {
|
try {
|
||||||
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||||
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag);
|
||||||
|
@ -198,12 +198,12 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getAudioLanguage() {
|
public String getAudioTrack() {
|
||||||
return audioLanguage;
|
return audioTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioLanguage(@Nullable final String audioLanguage) {
|
public void setAudioTrack(@Nullable final String audioLanguage) {
|
||||||
this.audioLanguage = audioLanguage;
|
this.audioTrack = audioLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface QualityResolver {
|
public interface QualityResolver {
|
||||||
|
@ -211,10 +211,4 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
||||||
|
|
||||||
int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality);
|
int getOverrideResolutionIndex(List<VideoStream> sortedVideos, String playbackQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface AudioLanguageResolver {
|
|
||||||
int getDefaultLanguageIndex(List<AudioStream> audioStreams);
|
|
||||||
|
|
||||||
int getOverrideLanguageIndex(List<AudioStream> audioStreams, String audioLanguage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,13 +118,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private static final int POPUP_MENU_ID_QUALITY = 69;
|
private static final int POPUP_MENU_ID_QUALITY = 69;
|
||||||
private static final int POPUP_MENU_ID_LANGUAGE = 70;
|
private static final int POPUP_MENU_ID_AUDIO_TRACK = 70;
|
||||||
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
|
||||||
private static final int POPUP_MENU_ID_CAPTION = 89;
|
private static final int POPUP_MENU_ID_CAPTION = 89;
|
||||||
|
|
||||||
protected boolean isSomePopupMenuVisible = false;
|
protected boolean isSomePopupMenuVisible = false;
|
||||||
private PopupMenu qualityPopupMenu;
|
private PopupMenu qualityPopupMenu;
|
||||||
private PopupMenu languagePopupMenu;
|
private PopupMenu audioTrackPopupMenu;
|
||||||
protected PopupMenu playbackSpeedPopupMenu;
|
protected PopupMenu playbackSpeedPopupMenu;
|
||||||
private PopupMenu captionPopupMenu;
|
private PopupMenu captionPopupMenu;
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
R.style.DarkPopupMenu);
|
R.style.DarkPopupMenu);
|
||||||
|
|
||||||
qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
|
qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
|
||||||
languagePopupMenu = new PopupMenu(themeWrapper, binding.languageTextView);
|
audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
|
||||||
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
|
||||||
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
|
captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
|
||||||
|
|
||||||
|
@ -194,8 +194,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
|
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked));
|
binding.qualityTextView.setOnClickListener(makeOnClickListener(this::onQualityClicked));
|
||||||
binding.languageTextView.setOnClickListener(
|
binding.audioTrackTextView.setOnClickListener(
|
||||||
makeOnClickListener(this::onAudioLanguageClicked));
|
makeOnClickListener(this::onAudioTracksClicked));
|
||||||
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
|
binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
|
||||||
|
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
binding.playbackSeekBar.setOnSeekBarChangeListener(this);
|
||||||
|
@ -272,7 +272,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
|
|
||||||
protected void deinitListeners() {
|
protected void deinitListeners() {
|
||||||
binding.qualityTextView.setOnClickListener(null);
|
binding.qualityTextView.setOnClickListener(null);
|
||||||
binding.languageTextView.setOnClickListener(null);
|
binding.audioTrackTextView.setOnClickListener(null);
|
||||||
binding.playbackSpeed.setOnClickListener(null);
|
binding.playbackSpeed.setOnClickListener(null);
|
||||||
binding.playbackSeekBar.setOnSeekBarChangeListener(null);
|
binding.playbackSeekBar.setOnSeekBarChangeListener(null);
|
||||||
binding.captionTextView.setOnClickListener(null);
|
binding.captionTextView.setOnClickListener(null);
|
||||||
|
@ -426,7 +426,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
|
binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
|
||||||
binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
|
||||||
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
binding.languageTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.audioTrackTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
|
||||||
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
|
||||||
|
@ -992,7 +992,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
private void updateStreamRelatedViews() {
|
private void updateStreamRelatedViews() {
|
||||||
player.getCurrentStreamInfo().ifPresent(info -> {
|
player.getCurrentStreamInfo().ifPresent(info -> {
|
||||||
binding.qualityTextView.setVisibility(View.GONE);
|
binding.qualityTextView.setVisibility(View.GONE);
|
||||||
binding.languageTextView.setVisibility(View.GONE);
|
binding.audioTrackTextView.setVisibility(View.GONE);
|
||||||
binding.playbackSpeed.setVisibility(View.GONE);
|
binding.playbackSpeed.setVisibility(View.GONE);
|
||||||
|
|
||||||
binding.playbackEndTime.setVisibility(View.GONE);
|
binding.playbackEndTime.setVisibility(View.GONE);
|
||||||
|
@ -1028,7 +1028,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
}
|
}
|
||||||
|
|
||||||
buildQualityMenu();
|
buildQualityMenu();
|
||||||
buildLanguageMenu();
|
buildAudioTrackMenu();
|
||||||
|
|
||||||
binding.qualityTextView.setVisibility(View.VISIBLE);
|
binding.qualityTextView.setVisibility(View.VISIBLE);
|
||||||
binding.surfaceView.setVisibility(View.VISIBLE);
|
binding.surfaceView.setVisibility(View.VISIBLE);
|
||||||
|
@ -1077,15 +1077,15 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
.ifPresent(s -> binding.qualityTextView.setText(s.getResolution()));
|
.ifPresent(s -> binding.qualityTextView.setText(s.getResolution()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildLanguageMenu() {
|
private void buildAudioTrackMenu() {
|
||||||
if (languagePopupMenu == null) {
|
if (audioTrackPopupMenu == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
languagePopupMenu.getMenu().removeGroup(POPUP_MENU_ID_LANGUAGE);
|
audioTrackPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_AUDIO_TRACK);
|
||||||
|
|
||||||
final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata())
|
final List<AudioStream> availableStreams = Optional.ofNullable(player.getCurrentMetadata())
|
||||||
.flatMap(MediaItemTag::getMaybeAudioLanguage)
|
.flatMap(MediaItemTag::getMaybeAudioTrack)
|
||||||
.map(MediaItemTag.AudioLanguage::getAudioStreams)
|
.map(MediaItemTag.AudioTrack::getAudioStreams)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (availableStreams == null || availableStreams.size() < 2) {
|
if (availableStreams == null || availableStreams.size() < 2) {
|
||||||
return;
|
return;
|
||||||
|
@ -1096,15 +1096,15 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
if (audioStream.getAudioTrackName() == null) {
|
if (audioStream.getAudioTrackName() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
languagePopupMenu.getMenu().add(POPUP_MENU_ID_LANGUAGE, i, Menu.NONE,
|
audioTrackPopupMenu.getMenu().add(POPUP_MENU_ID_AUDIO_TRACK, i, Menu.NONE,
|
||||||
audioStream.getAudioTrackName());
|
audioStream.getAudioTrackName());
|
||||||
}
|
}
|
||||||
|
|
||||||
player.getSelectedAudioStream()
|
player.getSelectedAudioStream()
|
||||||
.ifPresent(s -> binding.languageTextView.setText(s.getAudioTrackName()));
|
.ifPresent(s -> binding.audioTrackTextView.setText(s.getAudioTrackName()));
|
||||||
binding.languageTextView.setVisibility(View.VISIBLE);
|
binding.audioTrackTextView.setVisibility(View.VISIBLE);
|
||||||
languagePopupMenu.setOnMenuItemClickListener(this);
|
audioTrackPopupMenu.setOnMenuItemClickListener(this);
|
||||||
languagePopupMenu.setOnDismissListener(this);
|
audioTrackPopupMenu.setOnDismissListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildPlaybackSpeedMenu() {
|
private void buildPlaybackSpeedMenu() {
|
||||||
|
@ -1215,13 +1215,13 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
.ifPresent(binding.qualityTextView::setText);
|
.ifPresent(binding.qualityTextView::setText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAudioLanguageClicked() {
|
private void onAudioTracksClicked() {
|
||||||
languagePopupMenu.show();
|
audioTrackPopupMenu.show();
|
||||||
isSomePopupMenuVisible = true;
|
isSomePopupMenuVisible = true;
|
||||||
|
|
||||||
player.getSelectedAudioStream()
|
player.getSelectedAudioStream()
|
||||||
.map(AudioStream::getAudioTrackName)
|
.map(AudioStream::getAudioTrackName)
|
||||||
.ifPresent(binding.languageTextView::setText);
|
.ifPresent(binding.audioTrackTextView::setText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1238,8 +1238,8 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
|
||||||
onQualityItemClick(menuItem);
|
onQualityItemClick(menuItem);
|
||||||
return true;
|
return true;
|
||||||
} else if (menuItem.getGroupId() == POPUP_MENU_ID_LANGUAGE) {
|
} else if (menuItem.getGroupId() == POPUP_MENU_ID_AUDIO_TRACK) {
|
||||||
onLanguageItemClick(menuItem);
|
onAudioTrackItemClick(menuItem);
|
||||||
return true;
|
return true;
|
||||||
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
} else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
|
||||||
final int speedIndex = menuItem.getItemId();
|
final int speedIndex = menuItem.getItemId();
|
||||||
|
@ -1275,28 +1275,28 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
|
||||||
binding.qualityTextView.setText(menuItem.getTitle());
|
binding.qualityTextView.setText(menuItem.getTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLanguageItemClick(@NonNull final MenuItem menuItem) {
|
private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
|
||||||
final int menuItemIndex = menuItem.getItemId();
|
final int menuItemIndex = menuItem.getItemId();
|
||||||
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
@Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata();
|
||||||
if (currentMetadata == null || currentMetadata.getMaybeAudioLanguage().isEmpty()) {
|
if (currentMetadata == null || currentMetadata.getMaybeAudioTrack().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final MediaItemTag.AudioLanguage language =
|
final MediaItemTag.AudioTrack audioTrack =
|
||||||
currentMetadata.getMaybeAudioLanguage().get();
|
currentMetadata.getMaybeAudioTrack().get();
|
||||||
final List<AudioStream> availableStreams = language.getAudioStreams();
|
final List<AudioStream> availableStreams = audioTrack.getAudioStreams();
|
||||||
final int selectedStreamIndex = language.getSelectedAudioStreamIndex();
|
final int selectedStreamIndex = audioTrack.getSelectedAudioStreamIndex();
|
||||||
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
|
if (selectedStreamIndex == menuItemIndex || availableStreams.size() <= menuItemIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
player.saveStreamProgressState();
|
player.saveStreamProgressState();
|
||||||
final String newLanguage = availableStreams.get(menuItemIndex).getAudioTrackId();
|
final String newAudioTrack = availableStreams.get(menuItemIndex).getAudioTrackId();
|
||||||
player.setRecovery();
|
player.setRecovery();
|
||||||
player.setAudioLanguage(newLanguage);
|
player.setAudioTrack(newAudioTrack);
|
||||||
player.reloadPlayQueueManager();
|
player.reloadPlayQueueManager();
|
||||||
|
|
||||||
binding.languageTextView.setText(menuItem.getTitle());
|
binding.audioTrackTextView.setText(menuItem.getTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -188,6 +188,14 @@ public final class ListHelper {
|
||||||
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
videoOnlyStreams, ascendingOrder, preferVideoOnlyStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the list of audio streams and return a list with the preferred stream for
|
||||||
|
* each audio track. Streams are sorted with the preferred language in the first position.
|
||||||
|
*
|
||||||
|
* @param context the context to search for the track to give preference
|
||||||
|
* @param audioStreams the list of audio streams
|
||||||
|
* @return the sorted, filtered list
|
||||||
|
*/
|
||||||
public static List<AudioStream> getFilteredAudioStreams(
|
public static List<AudioStream> getFilteredAudioStreams(
|
||||||
@NonNull final Context context,
|
@NonNull final Context context,
|
||||||
@Nullable final List<AudioStream> audioStreams) {
|
@Nullable final List<AudioStream> audioStreams) {
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/languageTextView"
|
android:id="@+id/audioTrackTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="35dp"
|
android:layout_height="35dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
|
|
@ -18,6 +18,14 @@
|
||||||
android:visible="true"
|
android:visible="true"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_audio_track"
|
||||||
|
android:tooltipText="@string/audio_track"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="ifRoom">
|
||||||
|
<menu />
|
||||||
|
</item>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_mute"
|
android:id="@+id/action_mute"
|
||||||
android:icon="@drawable/ic_volume_off"
|
android:icon="@drawable/ic_volume_off"
|
||||||
|
|
|
@ -413,6 +413,8 @@
|
||||||
<string name="play_queue_remove">Remove</string>
|
<string name="play_queue_remove">Remove</string>
|
||||||
<string name="play_queue_stream_detail">Details</string>
|
<string name="play_queue_stream_detail">Details</string>
|
||||||
<string name="play_queue_audio_settings">Audio Settings</string>
|
<string name="play_queue_audio_settings">Audio Settings</string>
|
||||||
|
<string name="play_queue_audio_track">Audio: </string>
|
||||||
|
<string name="audio_track">Audio track</string>
|
||||||
<string name="hold_to_append">Hold to enqueue</string>
|
<string name="hold_to_append">Hold to enqueue</string>
|
||||||
<string name="show_channel_details">Show channel details</string>
|
<string name="show_channel_details">Show channel details</string>
|
||||||
<string name="enqueue_stream">Enqueue</string>
|
<string name="enqueue_stream">Enqueue</string>
|
||||||
|
|
Loading…
Reference in New Issue