That fuck shit the fascists are using
1/*
2 * Copyright (C) 2017 Whisper Systems
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17package org.tm.archive.video;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.util.AttributeSet;
22import android.view.Window;
23import android.view.WindowManager;
24import android.widget.FrameLayout;
25
26import androidx.annotation.NonNull;
27import androidx.annotation.Nullable;
28
29import androidx.annotation.OptIn;
30import androidx.media3.common.C;
31import androidx.media3.common.MediaItem;
32import androidx.media3.common.PlaybackException;
33import androidx.media3.common.Player;
34import androidx.media3.common.Tracks;
35import androidx.media3.common.util.UnstableApi;
36import androidx.media3.exoplayer.ExoPlayer;
37import androidx.media3.exoplayer.source.ClippingMediaSource;
38import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
39import androidx.media3.exoplayer.source.MediaSource;
40import androidx.media3.ui.AspectRatioFrameLayout;
41import androidx.media3.ui.LegacyPlayerControlView;
42import androidx.media3.ui.PlayerView;
43
44import org.signal.core.util.logging.Log;
45import org.tm.archive.R;
46import org.tm.archive.dependencies.ApplicationDependencies;
47import org.tm.archive.mediapreview.MediaPreviewPlayerControlView;
48import org.tm.archive.mms.VideoSlide;
49
50import java.util.Objects;
51import java.util.concurrent.TimeUnit;
52
53@OptIn(markerClass = UnstableApi.class)
54public class VideoPlayer extends FrameLayout {
55
56 @SuppressWarnings("unused")
57 private static final String TAG = Log.tag(VideoPlayer.class);
58
59 private final PlayerView exoView;
60 private final DefaultMediaSourceFactory mediaSourceFactory;
61
62 private ExoPlayer exoPlayer;
63 private LegacyPlayerControlView exoControls;
64 private Window window;
65 private PlayerStateCallback playerStateCallback;
66 private PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback;
67 private PlayerCallback playerCallback;
68 private boolean clipped;
69 private long clippedStartUs;
70 private ExoPlayerListener exoPlayerListener;
71 private Player.Listener playerListener;
72 private boolean muted;
73
74 public VideoPlayer(Context context) {
75 this(context, null);
76 }
77
78 public VideoPlayer(Context context, AttributeSet attrs) {
79 this(context, attrs, 0);
80 }
81
82 public VideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
83 super(context, attrs, defStyleAttr);
84
85 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VideoPlayer);
86 int videPlayerLayout = typedArray.getResourceId(R.styleable.VideoPlayer_playerLayoutId, R.layout.video_player);
87
88 typedArray.recycle();
89 inflate(context, videPlayerLayout, this);
90
91 this.mediaSourceFactory = new DefaultMediaSourceFactory(context);
92
93 this.exoView = findViewById(R.id.video_view);
94 this.exoControls = createPlayerControls(getContext());
95
96 this.exoPlayerListener = new ExoPlayerListener();
97 this.playerListener = new Player.Listener() {
98 @Override
99 public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
100 onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
101 }
102
103 @Override
104 public void onPlaybackStateChanged(int playbackState) {
105 onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
106 }
107
108 private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
109 if (playerCallback != null) {
110 switch (playbackState) {
111 case Player.STATE_READY:
112 playerCallback.onReady();
113 if (playWhenReady) {
114 playerCallback.onPlaying();
115 } else {
116 playerCallback.onStopped();
117 }
118 break;
119 case Player.STATE_ENDED:
120 playerCallback.onStopped();
121 break;
122 }
123 }
124 }
125
126 @Override
127 public void onPlayerError(@NonNull PlaybackException error) {
128 Log.w(TAG, "A player error occurred", error);
129 if (playerCallback != null) {
130 playerCallback.onError();
131 }
132 }
133 };
134 }
135
136 private LegacyPlayerControlView createPlayerControls(Context context) {
137 final LegacyPlayerControlView playerControlView = new LegacyPlayerControlView(context);
138 playerControlView.setShowTimeoutMs(-1);
139 playerControlView.setShowNextButton(false);
140 playerControlView.setShowPreviousButton(false);
141 return playerControlView;
142 }
143
144 private MediaItem mediaItem;
145
146 public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay, String poolTag) {
147 setVideoSource(videoSource, autoplay, poolTag, 0, 0);
148 }
149
150 public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay, String poolTag, long clipStartMs, long clipEndMs) {
151 if (exoPlayer == null) {
152 exoPlayer = ApplicationDependencies.getExoPlayerPool().require(poolTag);
153 exoPlayer.addListener(exoPlayerListener);
154 exoPlayer.addListener(playerListener);
155 exoView.setPlayer(exoPlayer);
156 exoControls.setPlayer(exoPlayer);
157 if (muted) {
158 mute();
159 }
160 }
161
162 mediaItem = MediaItem.fromUri(Objects.requireNonNull(videoSource.getUri())).buildUpon()
163 .setClippingConfiguration(getClippingConfiguration(clipStartMs, clipEndMs))
164 .build();
165
166 exoPlayer.setMediaItem(mediaItem);
167 exoPlayer.prepare();
168 exoPlayer.setPlayWhenReady(autoplay);
169 }
170
171 public void mute() {
172 this.muted = true;
173 if (exoPlayer != null) {
174 exoPlayer.setVolume(0f);
175 }
176 }
177
178 public void unmute() {
179 this.muted = false;
180 if (exoPlayer != null) {
181 exoPlayer.setVolume(1f);
182 }
183 }
184
185 public boolean hasAudioTrack() {
186 if (exoPlayer != null) {
187 Tracks tracks = exoPlayer.getCurrentTracks();
188 return tracks.containsType(C.TRACK_TYPE_AUDIO);
189 }
190
191 return false;
192 }
193
194 public boolean isInitialized() {
195 return exoPlayer != null;
196 }
197
198 public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
199 exoView.setResizeMode(resizeMode);
200 }
201
202 public boolean isPlaying() {
203 if (this.exoPlayer != null) {
204 return this.exoPlayer.isPlaying();
205 } else {
206 return false;
207 }
208 }
209
210 public void pause() {
211 if (this.exoPlayer != null) {
212 this.exoPlayer.setPlayWhenReady(false);
213 }
214 }
215
216 public void hideControls() {
217 if (this.exoView != null) {
218 this.exoView.hideController();
219 }
220 }
221
222 public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
223 if (this.exoView != null) {
224 this.exoView.setKeepContentOnPlayerReset(keepContentOnPlayerReset);
225 }
226 }
227
228 @Override
229 public void setOnClickListener(@Nullable OnClickListener l) {
230 if (this.exoView != null) {
231 this.exoView.setClickable(false);
232 }
233
234 super.setOnClickListener(l);
235 }
236
237 public @Nullable LegacyPlayerControlView getControlView() {
238 return this.exoControls;
239 }
240
241 public void setControlView(MediaPreviewPlayerControlView controller) {
242 exoControls = controller;
243 exoControls.setPlayer(exoPlayer);
244 }
245
246 public void stop() {
247 if (this.exoPlayer != null) {
248 exoPlayer.stop();
249 exoPlayer.clearMediaItems();
250 }
251 }
252
253 public void cleanup() {
254 stop();
255
256 if (this.exoPlayer != null) {
257 exoView.setPlayer(null);
258
259 if (exoPlayer.equals(exoControls.getPlayer())) {
260 exoControls.setPlayer(null);
261 }
262
263 exoPlayer.removeListener(playerListener);
264 exoPlayer.removeListener(exoPlayerListener);
265
266 ApplicationDependencies.getExoPlayerPool().pool(exoPlayer);
267 this.exoPlayer = null;
268 }
269 }
270
271 public void loopForever() {
272 if (this.exoPlayer != null) {
273 exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
274 }
275 }
276
277 public long getDuration() {
278 if (this.exoPlayer != null) {
279 return this.exoPlayer.getDuration();
280 }
281 return 0L;
282 }
283
284 public long getPlaybackPosition() {
285 if (this.exoPlayer != null) {
286 return this.exoPlayer.getCurrentPosition();
287 }
288 return 0L;
289 }
290
291 public long getPlaybackPositionUs() {
292 if (this.exoPlayer != null) {
293 return TimeUnit.MILLISECONDS.toMicros(this.exoPlayer.getCurrentPosition());
294 }
295 return -1L;
296 }
297
298 public void setPlaybackPosition(long positionMs) {
299 if (this.exoPlayer != null) {
300 this.exoPlayer.seekTo(positionMs);
301 }
302 }
303
304 public void clip(long fromUs, long toUs, boolean playWhenReady) {
305 if (this.exoPlayer != null && mediaItem != null) {
306 MediaSource mediaItemSource = mediaSourceFactory.createMediaSource(mediaItem);
307 ClippingMediaSource clippedSource = new ClippingMediaSource(mediaItemSource, fromUs, toUs);
308
309 exoPlayer.setMediaSource(clippedSource);
310 exoPlayer.prepare();
311 exoPlayer.setPlayWhenReady(playWhenReady);
312 clipped = true;
313 clippedStartUs = fromUs;
314 }
315 }
316
317 public void removeClip(boolean playWhenReady) {
318 if (exoPlayer != null && mediaItem != null) {
319 if (clipped) {
320 exoPlayer.setMediaItem(mediaItem);
321 exoPlayer.prepare();
322 clipped = false;
323 clippedStartUs = 0;
324 }
325 exoPlayer.setPlayWhenReady(playWhenReady);
326 }
327 }
328
329 public void setWindow(@Nullable Window window) {
330 this.window = window;
331 }
332
333 public void setPlayerStateCallbacks(@Nullable PlayerStateCallback playerStateCallback) {
334 this.playerStateCallback = playerStateCallback;
335 }
336
337 public void setPlayerCallback(PlayerCallback playerCallback) {
338 this.playerCallback = playerCallback;
339 }
340
341 public void setPlayerPositionDiscontinuityCallback(@NonNull PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback) {
342 this.playerPositionDiscontinuityCallback = playerPositionDiscontinuityCallback;
343 }
344
345 /**
346 * Resumes a paused video, or restarts if at end of video.
347 */
348 public void play() {
349 if (exoPlayer != null) {
350 exoPlayer.setPlayWhenReady(true);
351 if (exoPlayer.getCurrentPosition() >= exoPlayer.getDuration()) {
352 exoPlayer.seekTo(0);
353 }
354 }
355 }
356
357 private @NonNull MediaItem.ClippingConfiguration getClippingConfiguration(long startMs, long endMs) {
358 return startMs != endMs ? new MediaItem.ClippingConfiguration.Builder()
359 .setStartPositionMs(startMs)
360 .setEndPositionMs(endMs)
361 .build()
362 : MediaItem.ClippingConfiguration.UNSET;
363 }
364
365 private class ExoPlayerListener implements Player.Listener {
366
367 @Override
368 public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
369 onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
370 }
371
372 @Override
373 public void onPlaybackStateChanged(int playbackState) {
374 onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
375 }
376
377 private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
378 switch (playbackState) {
379 case Player.STATE_IDLE:
380 case Player.STATE_BUFFERING:
381 case Player.STATE_ENDED:
382 if (window != null) {
383 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
384 }
385 break;
386 case Player.STATE_READY:
387 if (window != null) {
388 if (playWhenReady) {
389 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
390 } else {
391 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
392 }
393 }
394 notifyPlayerReady();
395 break;
396 default:
397 break;
398 }
399 }
400
401 @Override
402 public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
403 @NonNull Player.PositionInfo newPosition,
404 int reason)
405 {
406 if (playerPositionDiscontinuityCallback != null) {
407 playerPositionDiscontinuityCallback.onPositionDiscontinuity(VideoPlayer.this, reason);
408 }
409 }
410
411 private void notifyPlayerReady() {
412 if (playerStateCallback != null) playerStateCallback.onPlayerReady();
413 }
414 }
415
416 public interface PlayerStateCallback {
417 void onPlayerReady();
418 }
419
420 public interface PlayerPositionDiscontinuityCallback {
421 void onPositionDiscontinuity(@NonNull VideoPlayer player, int reason);
422 }
423
424 public interface PlayerCallback {
425
426 default void onReady() {}
427
428 void onPlaying();
429
430 void onStopped();
431
432 void onError();
433 }
434}