From 5b2452ba001613fc64ab65dbe5f13652e2a275c9 Mon Sep 17 00:00:00 2001 From: vkay94 Date: Sat, 26 Dec 2020 14:04:40 +0100 Subject: [PATCH] Add stream segments to VideoPlayerImpl --- .../newpipe/info_list/StreamSegmentAdapter.kt | 70 ++++++++++++++ .../newpipe/info_list/StreamSegmentItem.kt | 63 +++++++++++++ .../newpipe/player/VideoPlayerImpl.java | 92 ++++++++++++++++++- .../ic_format_list_numbered_white_24.xml | 10 ++ app/src/main/res/layout-large-land/player.xml | 18 ++++ .../main/res/layout/item_stream_segment.xml | 64 +++++++++++++ app/src/main/res/layout/player.xml | 17 ++++ 7 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt create mode 100644 app/src/main/res/drawable/ic_format_list_numbered_white_24.xml create mode 100644 app/src/main/res/layout/item_stream_segment.xml diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt new file mode 100644 index 00000000000..eb2f3a7efca --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt @@ -0,0 +1,70 @@ +package org.schabi.newpipe.info_list + +import android.util.Log +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.GroupieViewHolder +import org.schabi.newpipe.extractor.stream.StreamInfo +import kotlin.math.max + +/** + * Custom RecyclerView.Adapter for [StreamSegmentItem] for handling selection state. + */ +class StreamSegmentAdapter( + private val listener: StreamSegmentListener +) : GroupAdapter() { + + var currentIndex: Int = 0 + private set + + /** + * Returns `true` if the provided [StreamInfo] contains segments, `false` otherwise. + */ + fun setItems(info: StreamInfo): Boolean { + if (info.streamSegments.isNotEmpty()) { + clear() + val list = arrayListOf() + info.streamSegments.forEach { + list.add(StreamSegmentItem(it, listener)) + } + addAll(list) + return true + } + return false + } + + fun selectSegment(segment: StreamSegmentItem) { + unSelectCurrentSegment() + currentIndex = max(0, getAdapterPosition(segment)) + segment.isSelected = true + segment.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT) + } + + fun selectSegmentAt(position: Int) { + try { + selectSegment(getGroupAtAdapterPosition(position) as StreamSegmentItem) + } catch (e: IndexOutOfBoundsException) { + // Just to make sure that getGroupAtAdapterPosition doesn't close the app + // Shouldn't happen since setItems is always called before select-methods but just in case + currentIndex = 0 + Log.e("StreamSegmentAdapter", "selectSegmentAt: ${e.message}") + } + } + + private fun unSelectCurrentSegment() { + try { + val segmentItem = getGroupAtAdapterPosition(currentIndex) as StreamSegmentItem + currentIndex = 0 + segmentItem.isSelected = false + segmentItem.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT) + } catch (e: IndexOutOfBoundsException) { + // Just to make sure that getGroupAtAdapterPosition doesn't close the app + // Shouldn't happen since setItems is always called before select-methods but just in case + currentIndex = 0 + Log.e("StreamSegmentAdapter", "unSelectCurrentSegment: ${e.message}") + } + } + + interface StreamSegmentListener { + fun onItemClick(item: StreamSegmentItem, secondsInMillis: Long) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt new file mode 100644 index 00000000000..056d3f650eb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt @@ -0,0 +1,63 @@ +package org.schabi.newpipe.info_list + +import android.widget.ImageView +import android.widget.TextView +import com.nostra13.universalimageloader.core.ImageLoader +import com.xwray.groupie.GroupieViewHolder +import com.xwray.groupie.Item +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.stream.StreamSegment +import org.schabi.newpipe.util.ImageDisplayConstants +import java.util.concurrent.TimeUnit + +class StreamSegmentItem( + private val item: StreamSegment, + private val onClick: StreamSegmentAdapter.StreamSegmentListener +) : Item() { + + companion object { + const val PAYLOAD_SELECT = 1 + } + + var isSelected = false + + override fun bind(viewHolder: GroupieViewHolder, position: Int) { + item.previewUrl?.let { + ImageLoader.getInstance().displayImage( + it, viewHolder.root.findViewById(R.id.previewImage), + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS + ) + } + viewHolder.root.findViewById(R.id.textViewTitle).text = item.title + viewHolder.root.findViewById(R.id.textViewStartSeconds).text = + secondsToString(item.startTimeSeconds.toLong()) + viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds * 1000L) } + viewHolder.root.isSelected = isSelected + } + + override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) { + if (payloads.contains(PAYLOAD_SELECT)) { + viewHolder.root.isSelected = isSelected + return + } + super.bind(viewHolder, position, payloads) + } + + private fun secondsToString(seconds: Long): String { + val hours = TimeUnit.SECONDS.toHours(seconds) + val minutes = TimeUnit.SECONDS.toMinutes(seconds) + .minus(TimeUnit.HOURS.toMinutes(hours)) + val sec = seconds + .minus(TimeUnit.HOURS.toSeconds(hours)) + .minus(TimeUnit.MINUTES.toSeconds(minutes)) + + return if (hours == 0L) String.format("%02d:%02d", minutes, sec) + else String.format("%02d:%02d:%02d", hours, minutes, sec) + } + + override fun getLayout() = R.layout.item_stream_segment + + interface ClickListener { + fun onClick(segmentItem: StreamSegmentItem, positionInMillis: Long) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index a304b44300b..84d3bc40904 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -78,6 +78,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; @@ -100,6 +101,7 @@ import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.info_list.StreamSegmentAdapter; import java.util.List; @@ -155,6 +157,7 @@ public class VideoPlayerImpl extends VideoPlayer private ImageView brightnessImageView; private TextView resizingIndicator; private ImageButton queueButton; + private ImageButton segmentsButton; private ImageButton repeatButton; private ImageButton shuffleButton; private ImageButton playWithKodi; @@ -171,11 +174,13 @@ public class VideoPlayerImpl extends VideoPlayer private RelativeLayout queueLayout; private ImageButton itemsListCloseButton; private RecyclerView itemsList; + private StreamSegmentAdapter segmentAdapter; private ItemTouchHelper itemTouchHelper; private RelativeLayout playerOverlays; private boolean queueVisible; + private boolean segmentsVisible; private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; private ImageButton moreOptionsButton; @@ -291,6 +296,7 @@ public void initViews(final View view) { this.brightnessImageView = view.findViewById(R.id.brightnessImageView); this.resizingIndicator = view.findViewById(R.id.resizing_indicator); this.queueButton = view.findViewById(R.id.queueButton); + this.segmentsButton = view.findViewById(R.id.segmentsButton); this.repeatButton = view.findViewById(R.id.repeatButton); this.shuffleButton = view.findViewById(R.id.shuffleButton); this.playWithKodi = view.findViewById(R.id.playWithKodi); @@ -320,10 +326,20 @@ public void initViews(final View view) { titleTextView.setSelected(true); channelTextView.setSelected(true); + segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener()); + // Prevent hiding of bottom sheet via swipe inside queue this.itemsList.setNestedScrollingEnabled(false); } + private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { + return (item, secondsInMillis) -> { + segmentAdapter.selectSegment(item); + getPlayer().seekTo(secondsInMillis); + triggerProgressUpdate(); + }; + } + @Override protected void setupSubtitleView(final @NonNull SubtitleView view, final float captionScale, @@ -354,6 +370,7 @@ private void setupElementsVisibility() { getResizeView().setVisibility(View.GONE); getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); queueButton.setVisibility(View.GONE); + segmentsButton.setVisibility(View.GONE); moreOptionsButton.setVisibility(View.GONE); getTopControlsRoot().setOrientation(LinearLayout.HORIZONTAL); primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; @@ -453,6 +470,7 @@ public void initListeners() { getRootView().setOnTouchListener(listener); queueButton.setOnClickListener(this); + segmentsButton.setOnClickListener(this); repeatButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); @@ -775,6 +793,9 @@ public void onClick(final View v) { } else if (v.getId() == queueButton.getId()) { onQueueClicked(); return; + } else if (v.getId() == segmentsButton.getId()) { + onSegmentsClicked(); + return; } else if (v.getId() == repeatButton.getId()) { onRepeatClicked(); return; @@ -850,8 +871,25 @@ private void onQueueClicked() { itemsList.scrollToPosition(playQueue.getIndex()); } + private void onSegmentsClicked() { + segmentsVisible = true; + + hideSystemUIIfNeeded(); + buildSegments(); + + hideControls(0, 0); + queueLayout.requestFocus(); + animateView(queueLayout, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + final int adapterPosition = getNearestStreamSegmentPosition(getPlayer() + .getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + itemsList.scrollToPosition(adapterPosition); + } + public void onQueueClosed() { - if (!queueVisible) { + if (!queueVisible && !segmentsVisible) { return; } @@ -862,6 +900,7 @@ public void onQueueClosed() { queueLayout.setTranslationY(-queueLayout.getHeight() * 5); }); queueVisible = false; + segmentsVisible = false; playPauseButton.requestFocus(); } @@ -1463,6 +1502,14 @@ private void showOrHideButtons() { playNextButton.setAlpha(showNext ? 1.0f : 0.0f); queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); queueButton.setAlpha(showQueue ? 1.0f : 0.0f); + + boolean showSegment = false; + if (getCurrentMetadata() != null) { + showSegment = getCurrentMetadata().getMetadata().getStreamSegments().size() > 0 + && !popupPlayerSelected(); + } + segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE); + segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f); } private void showSystemUIPartially() { @@ -1548,9 +1595,40 @@ private void buildQueue() { playQueueAdapter.setSelectedListener(getOnSelectedListener()); + shuffleButton.setVisibility(View.VISIBLE); + repeatButton.setVisibility(View.VISIBLE); itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); } + private void buildSegments() { + itemsList.setAdapter(segmentAdapter); + itemsList.setClickable(true); + itemsList.clearOnScrollListeners(); + + if (getCurrentMetadata() != null) { + segmentAdapter.setItems(getCurrentMetadata().getMetadata()); + } + + shuffleButton.setVisibility(View.GONE); + repeatButton.setVisibility(View.GONE); + itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); + } + + private int getNearestStreamSegmentPosition(final long playbackPosition) { + int nearestPosition = 0; + final List segments = getCurrentMetadata().getMetadata() + .getStreamSegments(); + + for (int i = 0; i < segments.size(); i++) { + if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) { + break; + } + nearestPosition++; + } + + return Math.max(0, nearestPosition - 1); + } + public void useVideoSource(final boolean video) { if (playQueue == null || audioOnly == !video || audioPlayerSelected()) { return; @@ -1976,6 +2054,15 @@ private void updateMetadata() { if (activityListener != null && getCurrentMetadata() != null) { activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); } + if (getCurrentMetadata() != null && segmentsVisible + && segmentAdapter.setItems(getCurrentMetadata().getMetadata())) { + final int adapterPosition = getNearestStreamSegmentPosition(getPlayer() + .getCurrentPosition()); + segmentAdapter.selectSegmentAt(adapterPosition); + itemsList.scrollToPosition(adapterPosition); + } else if (segmentsVisible) { + onQueueClosed(); + } } private void updatePlayback() { @@ -1997,6 +2084,9 @@ private void updateProgress(final int currentProgress, final int duration, if (activityListener != null) { activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); } + if (segmentsVisible) { + segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress)); + } } void stopActivityBinding() { diff --git a/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml b/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml new file mode 100644 index 00000000000..429616ec98a --- /dev/null +++ b/app/src/main/res/drawable/ic_format_list_numbered_white_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 145d1e2f891..5473db8e4cc 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -193,6 +193,24 @@ tools:ignore="ContentDescription,RtlHardcoded" tools:visibility="visible" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index 11b8bc5db36..847654de361 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -193,6 +193,23 @@ app:srcCompat="@drawable/ic_list_white_24dp" tools:ignore="ContentDescription,RtlHardcoded" /> + +