├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── timlineview
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── timlineview
│ │ │ ├── Android.java
│ │ │ ├── ItemSelectListener.java
│ │ │ ├── MainActivity.java
│ │ │ ├── NewActivity.java
│ │ │ ├── ThumbLoader.java
│ │ │ ├── TrimInfo.java
│ │ │ ├── VideoAdapter.java
│ │ │ ├── VideoFrameAdapter.java
│ │ │ └── VideoFrameAdapter2.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── preview.xml
│ │ ├── preview2.xml
│ │ └── preview3.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ids.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── example
│ └── timlineview
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── video
│ │ └── timeline
│ │ └── ExampleInstrumentedTest.java
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── video
│ └── timeline
│ ├── ExoPlayerFactory.java
│ ├── FetchCallback.java
│ ├── ImageLoader.java
│ ├── RetroInstance.java
│ ├── RetroSurfaceListener.java
│ ├── TimelineViewFace.java
│ ├── VideoFrameCache.java
│ ├── VideoMetadata.java
│ ├── VideoRendererOnlyFactory.java
│ ├── VideoTimeLine.java
│ ├── android
│ ├── MRetriever.java
│ └── MediaRetrieverAdapter.java
│ ├── render
│ ├── BaseGLRenderer.java
│ ├── FBOHandler.java
│ ├── GlRenderer.java
│ ├── RetroRenderer.java
│ ├── TextureProgram.java
│ └── TimelineGlSurfaceView.java
│ └── tools
│ ├── FileHelper.java
│ ├── GlUtils.java
│ ├── Loggy.java
│ └── MediaHelper.java
├── screens
└── 1_shot.jpg
└── settings.gradle
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | /.history
17 | app/.history
18 | library/.history
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 chemicalbird
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ExoPlayerTimelineView
2 |
3 | If you already use ExoPlayer in your project and need to extract video
4 | frames and show them as a **timeline view** either scrollable or in a
5 | fixed-width mode then you're in the right place.
6 |
7 |
8 |
9 |
10 |
11 | You might already be familiar with `MediaMetadataRetriever` api and its
12 | `getFrameAtTime(..)` method, I included adapter implementation(it's not
13 | meant for production use) for comparison with `RetroInstance` API, which
14 | caches frames internally and won't init any mediaCodec resource if
15 | unnecessary. And it's running significantly faster.
16 |
17 | ### Installation
18 | Add this to your application module, inside *dependencies* block.
19 | ```sh
20 | dependencies {
21 | implementation 'com.chemicalbird.android:videotimelineview:0.0.4'
22 | }
23 | ```
24 |
25 | ### Usage example
26 |
27 | 1. For fixed frame list there is already an implementation of
28 | `GLSurfaceView` that you can use out of the box. Your layout and code
29 | goes like this.
30 |
31 | ```sh
32 |
37 | ```
38 | ```sh
39 | VideoTimeLine.with(fileUri).show(glSurfaceView);
40 | ```
41 |
42 | 2. To create a frame grabber MediaRetriever implementation use
43 | `RetroInstance`, for example pass it to your RecyclerView.Adapter. To
44 | get a frame at specific time call
45 | `retroInstance.load(presentationTime, callback)`. Checkout Sample
46 | project for more details.
47 |
48 | ```sh
49 | RetroInstance retroInstance = new RetroInstance.Builder(context, mediaUri).setFrameSizeDp(180).create();
50 |
51 | // in adapter
52 | retroInstance.load(position * frameDuration, holder.hashCode(),
53 | file -> imageLoader.load(file, imageView));
54 | ```
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 | buildToolsVersion "29.0.3"
6 |
7 | defaultConfig {
8 | applicationId "com.example.timlineview"
9 | minSdkVersion 18
10 | targetSdkVersion 29
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility 1.8
26 | targetCompatibility 1.8
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: 'libs', include: ['*.jar'])
32 |
33 | implementation 'androidx.appcompat:appcompat:1.1.0'
34 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
36 | testImplementation 'junit:junit:4.12'
37 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
39 |
40 | implementation project(':library')
41 |
42 | implementation 'com.parse.bolts:bolts-tasks:1.4.0'
43 | implementation ('com.squareup.picasso:picasso:2.71828')
44 | }
45 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/timlineview/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("com.example.myapplication", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/Android.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.pm.PackageManager;
6 | import android.database.Cursor;
7 | import android.os.Build;
8 | import android.provider.MediaStore;
9 |
10 | import androidx.core.app.ActivityCompat;
11 |
12 | import java.io.File;
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | import bolts.Task;
17 |
18 | public class Android {
19 |
20 | public static Task> queryRecentVideos(Context c, int limit) {
21 | return
22 | Task.call(() -> {
23 | String sortOrder = MediaStore.Video.Media.DATE_MODIFIED + " DESC Limit " + limit;
24 | Cursor cursor = c.getContentResolver().query(
25 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
26 | null,
27 | null,
28 | null,
29 | sortOrder
30 | );
31 |
32 | List files = new ArrayList<>();
33 | while (cursor != null && cursor.moveToNext()) {
34 |
35 | String path = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.DATA));
36 | if (new File(path).exists()) {
37 | files.add(path);
38 | }
39 | }
40 |
41 | return files;
42 | }, Task.BACKGROUND_EXECUTOR);
43 |
44 | }
45 |
46 | public static int dpToPx(Context context, float dp) {
47 | float density = context.getResources().getDisplayMetrics().density;
48 | return (int) (dp * density + 0.5f);
49 | }
50 |
51 | public static boolean checkPermission(Activity activity, String permission, int requestCode) {
52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
53 | if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
54 | activity.requestPermissions(new String[]{permission}, requestCode);
55 | return false;
56 | }
57 | }
58 |
59 | return true;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/ItemSelectListener.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | interface ItemSelectListener {
4 | void onSelect(String path);
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import androidx.appcompat.app.AppCompatActivity;
4 | import androidx.recyclerview.widget.GridLayoutManager;
5 | import androidx.recyclerview.widget.RecyclerView;
6 |
7 | import android.Manifest;
8 | import android.content.Intent;
9 | import android.os.Bundle;
10 | import android.view.View;
11 | import android.widget.CheckBox;
12 | import android.widget.CompoundButton;
13 |
14 | import java.util.ArrayList;
15 |
16 | import bolts.Task;
17 |
18 | public class MainActivity extends AppCompatActivity implements ItemSelectListener {
19 |
20 | private boolean groupMode;
21 | private ArrayList videos = new ArrayList<>();
22 |
23 | @Override
24 | protected void onCreate(Bundle savedInstanceState) {
25 | super.onCreate(savedInstanceState);
26 | setContentView(R.layout.activity_main);
27 |
28 | RecyclerView rv = findViewById(R.id.video_list);
29 | rv.setLayoutManager(new GridLayoutManager(this, 3));
30 |
31 | if (Android.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, 0)) {
32 | Android.queryRecentVideos(this, 100).continueWith((cont) -> {
33 | if (cont.getResult() != null) {
34 | VideoAdapter adapter = new VideoAdapter(MainActivity.this,
35 | cont.getResult(), MainActivity.this);
36 | rv.setAdapter(adapter);
37 | }
38 | return null;
39 | }, Task.UI_THREAD_EXECUTOR);
40 | }
41 |
42 | CheckBox mutlipleVCheckbox = findViewById(R.id.multiple_vids);
43 | mutlipleVCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
44 | @Override
45 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
46 | groupMode = isChecked;
47 | }
48 | });
49 |
50 | findViewById(R.id.multiple_sel_btn).setOnClickListener(v -> open());
51 | }
52 |
53 | @Override
54 | public void onSelect(String path) {
55 | videos.add(path);
56 | if (!groupMode) {
57 | open();
58 | } else {
59 | findViewById(R.id.multiple_sel_btn).setVisibility(videos.size() > 0 ? View.VISIBLE : View.GONE);
60 | }
61 | }
62 |
63 | private void open() {
64 | Intent intent = new Intent(this, NewActivity.class);
65 | intent.putExtra("file_uri", videos);
66 | startActivity(intent);
67 | videos.clear();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/NewActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.graphics.Color;
4 | import android.graphics.drawable.ColorDrawable;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.os.Environment;
8 | import android.view.View;
9 | import android.webkit.URLUtil;
10 | import android.widget.CheckBox;
11 | import android.widget.TextView;
12 |
13 | import androidx.annotation.NonNull;
14 | import androidx.annotation.Nullable;
15 | import androidx.appcompat.app.AppCompatActivity;
16 | import androidx.recyclerview.widget.LinearLayoutManager;
17 | import androidx.recyclerview.widget.RecyclerView;
18 |
19 | import com.google.android.exoplayer2.Format;
20 | import com.google.android.exoplayer2.SimpleExoPlayer;
21 | import com.google.android.exoplayer2.analytics.AnalyticsListener;
22 | import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
23 | import com.google.android.exoplayer2.source.MediaSource;
24 | import com.google.android.exoplayer2.source.MediaSourceEventListener;
25 | import com.google.android.exoplayer2.source.ProgressiveMediaSource;
26 | import com.google.android.exoplayer2.ui.PlayerView;
27 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
28 | import com.google.android.exoplayer2.util.MimeTypes;
29 | import com.squareup.picasso.Picasso;
30 | import com.video.timeline.ImageLoader;
31 | import com.video.timeline.RetroInstance;
32 | import com.video.timeline.VideoMetadata;
33 | import com.video.timeline.render.TimelineGlSurfaceView;
34 | import com.video.timeline.VideoTimeLine;
35 | import com.video.timeline.android.MediaRetrieverAdapter;
36 | import com.video.timeline.tools.MediaHelper;
37 |
38 | import java.io.File;
39 | import java.util.ArrayList;
40 | import java.util.List;
41 |
42 | public class NewActivity extends AppCompatActivity implements View.OnClickListener {
43 | private SimpleExoPlayer player;
44 |
45 | private VideoTimeLine fixedVideoTimeline;
46 |
47 | private ImageLoader picassoLoader = (file, view) -> {
48 | if (file == null || !file.exists()) {
49 | view.setImageDrawable(new ColorDrawable(Color.LTGRAY));
50 | } else {
51 | Picasso.get().load(Uri.fromFile(file))
52 | .placeholder(new ColorDrawable(Color.LTGRAY)).into(view);
53 | }
54 | };
55 | private RecyclerView defaultListView;
56 | private RecyclerView retroListView;
57 | private RetroInstance retroInstance;
58 |
59 | @Override
60 | protected void onCreate(@Nullable Bundle savedInstanceState) {
61 | super.onCreate(savedInstanceState);
62 |
63 | setContentView(R.layout.preview3);
64 | findViewById(R.id.show_fixed).setOnClickListener(this);
65 |
66 | PlayerView playerView = findViewById(R.id.playerView);
67 | TextView infoView = findViewById(R.id.info_view);
68 |
69 | List videos = getIntent().getStringArrayListExtra("file_uri");
70 | if (videos.size() == 1) {
71 | String fileUri = videos.get(0);
72 | MediaSource mediaSource = new ProgressiveMediaSource.Factory
73 | (new DefaultDataSourceFactory(this, "geo"), new DefaultExtractorsFactory())
74 | .createMediaSource(URLUtil.isNetworkUrl(fileUri) ? Uri.parse(fileUri) : Uri.fromFile(new File(fileUri)));
75 |
76 | player = new SimpleExoPlayer.Builder(this).build();
77 | player.prepare(mediaSource);
78 | playerView.setPlayer(player);
79 | player.addAnalyticsListener(new AnalyticsListener() {
80 | @Override
81 | public void onDownstreamFormatChanged(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) {
82 |
83 | }
84 |
85 | @Override
86 | public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {
87 | if (MimeTypes.isVideo(format.sampleMimeType)) {
88 | infoView.setText(format.width + "x" + format.height + " FrameR:" + format.frameRate + " BitR:" + format.bitrate);
89 | }
90 | }
91 | });
92 |
93 | TimelineGlSurfaceView glSurfaceView = findViewById(R.id.fixed_thumb_list);
94 | fixedVideoTimeline = VideoTimeLine.with(fileUri).into(glSurfaceView);
95 | }
96 |
97 | playerView.getVideoSurfaceView().setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady()));
98 |
99 | findViewById(R.id.cache_clear).setOnClickListener(v -> {
100 | deleteCache(new File(getCacheDir(), "exo_frames"));
101 | deleteCache(new File(getCacheDir(), ".thumbs"));
102 | deleteCache(new File(getCacheDir(), ".frames"));
103 | deleteCache(new File(Environment.getExternalStorageDirectory(), "ACache"));
104 | });
105 |
106 | defaultListView = findViewById(R.id.default_list_view);
107 | defaultListView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
108 | findViewById(R.id.show_default).setOnClickListener(this);
109 |
110 | retroListView = findViewById(R.id.retro_list_view);
111 | retroListView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
112 | findViewById(R.id.show_retro).setOnClickListener(this);
113 |
114 | retroListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
115 | @Override
116 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
117 | if (newState == RecyclerView.SCROLL_STATE_IDLE) {
118 | // long seekPos = (long) (recyclerView.computeHorizontalScrollOffset() * 1F /
119 | // recyclerView.computeHorizontalScrollRange()
120 | // * player.getDuration());
121 | // player.seekTo(seekPos);
122 | findViewById(R.id.show_retro).setEnabled(true);
123 | } else {
124 | findViewById(R.id.show_retro).setEnabled(false);
125 | }
126 | }
127 | });
128 |
129 | defaultListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
130 | @Override
131 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
132 | findViewById(R.id.show_default).setEnabled(newState == RecyclerView.SCROLL_STATE_IDLE);
133 | }
134 | });
135 | }
136 |
137 | private void deleteCache(File dir) {
138 | if (dir.exists() && dir.isDirectory()) {
139 | File[] files = dir.listFiles();
140 | if (files == null) return;
141 |
142 | for (File el: files) {
143 | el.delete();
144 | }
145 | }
146 | }
147 |
148 | @Override
149 | protected void onDestroy() {
150 | super.onDestroy();
151 | if (player != null) {
152 | player.release();
153 | }
154 |
155 | if (fixedVideoTimeline != null) {
156 | fixedVideoTimeline.destroy();
157 | }
158 |
159 | if (retroInstance != null) {
160 | retroInstance.onDestroy();
161 | }
162 | }
163 |
164 | @Override
165 | public void onClick(View v) {
166 | if (v.getId() == R.id.show_fixed) {
167 | fixedVideoTimeline.start();
168 | } else if (v.getId() == R.id.show_default) {
169 | List videos = getIntent().getStringArrayListExtra("file_uri");
170 | if (videos.size() == 1) {
171 | MediaRetrieverAdapter adapter = new MediaRetrieverAdapter(this, videos.get(0), 2000, 180, picassoLoader);
172 | defaultListView.setAdapter(adapter);
173 | }
174 | } else if (v.getId() == R.id.show_retro) {
175 | showBVariant();
176 | }
177 | }
178 |
179 | private void showBVariant() {
180 | CheckBox softRetroCheckbox = findViewById(R.id.soft_retro);
181 | List videos = getIntent().getStringArrayListExtra("file_uri");
182 |
183 | List mets = new ArrayList<>();
184 |
185 | for (String video: videos) {
186 | VideoMetadata videoMetadata = new VideoMetadata();
187 | MediaHelper.getVideoMets(this, video, videoMetadata);
188 | mets.add(videoMetadata);
189 | }
190 |
191 | if (retroInstance != null) {
192 | retroInstance.onDestroy();
193 | }
194 |
195 | retroInstance = new RetroInstance.Builder(this)
196 | .softwareDecoder(softRetroCheckbox.isChecked())
197 | .setFrameSizeDp(180)
198 | .create();
199 |
200 | retroListView.setAdapter(
201 | new VideoFrameAdapter2(retroInstance, 2000, picassoLoader, videos, mets));
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/ThumbLoader.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.content.Context;
4 | import android.graphics.SurfaceTexture;
5 | import android.net.Uri;
6 | import android.os.Handler;
7 | import android.os.HandlerThread;
8 | import android.os.Message;
9 | import android.os.Process;
10 | import android.os.SystemClock;
11 | import androidx.annotation.Nullable;
12 | import android.util.Log;
13 | import android.view.Surface;
14 | import android.view.SurfaceHolder;
15 | import android.view.TextureView;
16 |
17 | import com.google.android.exoplayer2.C;
18 | import com.google.android.exoplayer2.DefaultLoadControl;
19 | import com.google.android.exoplayer2.ExoPlaybackException;
20 | import com.google.android.exoplayer2.Format;
21 | import com.google.android.exoplayer2.PlayerMessage;
22 | import com.google.android.exoplayer2.Renderer;
23 | import com.google.android.exoplayer2.RendererCapabilities;
24 | import com.google.android.exoplayer2.RendererConfiguration;
25 | import com.google.android.exoplayer2.SeekParameters;
26 | import com.google.android.exoplayer2.Timeline;
27 | import com.google.android.exoplayer2.decoder.DecoderCounters;
28 | import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
29 | import com.google.android.exoplayer2.metadata.Metadata;
30 | import com.google.android.exoplayer2.metadata.MetadataOutput;
31 | import com.google.android.exoplayer2.source.DefaultMediaSourceEventListener;
32 | import com.google.android.exoplayer2.source.EmptySampleStream;
33 | import com.google.android.exoplayer2.source.ExtractorMediaSource;
34 | import com.google.android.exoplayer2.source.MediaPeriod;
35 | import com.google.android.exoplayer2.source.MediaSource;
36 | import com.google.android.exoplayer2.source.SampleStream;
37 | import com.google.android.exoplayer2.source.TrackGroupArray;
38 | import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
39 | import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
40 | import com.google.android.exoplayer2.trackselection.TrackSelection;
41 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
42 | import com.google.android.exoplayer2.trackselection.TrackSelector;
43 | import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
44 | import com.google.android.exoplayer2.upstream.DataSource;
45 | import com.google.android.exoplayer2.upstream.DataSpec;
46 | import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
47 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
48 | import com.google.android.exoplayer2.upstream.TransferListener;
49 | import com.google.android.exoplayer2.video.VideoListener;
50 | import com.google.android.exoplayer2.video.VideoRendererEventListener;
51 | import com.video.timeline.VideoRendererOnlyFactory;
52 |
53 | import java.io.IOException;
54 |
55 |
56 | public class ThumbLoader implements PlayerMessage.Sender, Handler.Callback, MediaPeriod.Callback, MediaSource.MediaSourceCaller, TransferListener, TrackSelector.InvalidationListener {
57 |
58 | private HandlerThread internalPlaybackThread;
59 | private Handler defaultHandler;
60 | private Context context;
61 | private DefaultLoadControl control;
62 | private ExtractorMediaSource mediaSource;
63 | private boolean periodPrepared;
64 | private Renderer videoRenderer;
65 | private long rendererPositionUs;
66 | private MediaPeriod mediaPeriod;
67 | private DefaultTrackSelector trackSelector;
68 | private RendererCapabilities rendererCapabilities;
69 | private TrackSelectorResult selectorResult;
70 | private VideoListener listener;
71 | private long pendingSeekPositinoUs;
72 | private long pendingEndPositionUs;
73 | private DefaultExtractorsFactory extractorsFactory;
74 |
75 | private Timeline timeline;
76 | private TrimInfo trimInfo;
77 |
78 | private Handler eventHandler;
79 | private Timeline.Window window;
80 |
81 | public ThumbLoader(final Context context, Handler handler) {
82 | this.context = context;
83 | createWorkerThread();
84 |
85 | control = new DefaultLoadControl();
86 |
87 | extractorsFactory = new DefaultExtractorsFactory();
88 |
89 | eventHandler = handler;
90 |
91 | trackSelector = new DefaultTrackSelector(context);
92 |
93 | trackSelector.init(ThumbLoader.this, new DefaultBandwidthMeter.Builder(context).build());
94 |
95 | ComponentListener listener = new ComponentListener();
96 |
97 | videoRenderer = new VideoRendererOnlyFactory(context, false).
98 | createRenderers(eventHandler, listener, null, null, listener,null)[0];
99 |
100 | rendererCapabilities = videoRenderer.getCapabilities();
101 |
102 | window = new Timeline.Window();
103 | }
104 |
105 | private void createWorkerThread() {
106 | internalPlaybackThread = new HandlerThread("Thumbnail:Thread",
107 | Process.THREAD_PRIORITY_AUDIO);
108 | internalPlaybackThread.start();
109 | defaultHandler = new Handler(internalPlaybackThread.getLooper(), this);
110 | }
111 |
112 | private PlayerMessage createMessage(Renderer renderer) {
113 | return new PlayerMessage(this, renderer, Timeline.EMPTY, 0, defaultHandler);
114 | }
115 |
116 | public void setVideoSurface(Surface surface) {
117 | insureWorker();
118 | createMessage(videoRenderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send();
119 | }
120 |
121 | public void prepare(TrimInfo trimInfo) {
122 | insureWorker();
123 | if (videoRenderer == null) {
124 | ComponentListener listener = new ComponentListener();
125 | videoRenderer = new VideoRendererOnlyFactory(context, false).
126 | createRenderers(eventHandler, listener, null, null, listener,null)[0];
127 |
128 | rendererCapabilities = videoRenderer.getCapabilities();
129 | }
130 |
131 | defaultHandler.sendMessageDelayed(defaultHandler.obtainMessage(1, trimInfo), 100); //-= prepare
132 | }
133 |
134 | public void release() {
135 | if (mediaSource == null) return;
136 | defaultHandler.sendEmptyMessage(11);
137 | }
138 |
139 | public boolean isStateIdentical(String url) {
140 | return trimInfo != null && url.equals(trimInfo.url);
141 | }
142 |
143 | @Override
144 | public void onPrepared(MediaPeriod mediaPeriod) {
145 | defaultHandler.obtainMessage(3, mediaPeriod).sendToTarget();
146 | }
147 |
148 | @Override
149 | public void onContinueLoadingRequested(MediaPeriod source) {
150 | defaultHandler.obtainMessage(4).sendToTarget();
151 | Log.d("killer_john", "onContinueLoadingRequested");
152 | }
153 |
154 | public long getDurationInSeconds() {
155 | long endPos = pendingEndPositionUs;
156 | if (pendingEndPositionUs == C.TIME_END_OF_SOURCE) {
157 | endPos = timeline.getWindow(0, window).getDurationUs();
158 | }
159 |
160 | return (endPos - pendingSeekPositinoUs);
161 |
162 | }
163 |
164 | public void seekTo(long s) {
165 | if (s + rendererPositionUs < timeline.getWindow(0, window).getDurationUs()) {
166 | defaultHandler.obtainMessage(10, s + rendererPositionUs).sendToTarget();
167 | } else {
168 | onFinishedLoading();
169 | }
170 | }
171 |
172 | private void prepareInternal() {
173 | // DataSource.Factory cacheDataSource = new CacheDataSourceFactory(Singles.playerCache, new DefaultDataSourceFactory(context, AndroidUtil.getRealUserAgent()));
174 | DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, "exo");
175 | mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory)
176 | // .setContinueLoadingCheckIntervalBytes(100)
177 | .setExtractorsFactory(extractorsFactory)
178 | .createMediaSource(Uri.parse(trimInfo.url));
179 |
180 | mediaSource.prepareSource(this, this);
181 |
182 | mediaPeriod = mediaSource.createPeriod(new
183 | MediaSource.MediaPeriodId(/* periodUid= */ new Object()), control.getAllocator(), 0);
184 |
185 | mediaPeriod.prepare(this, 0);
186 |
187 | mediaSource.addEventListener(eventHandler, new DefaultMediaSourceEventListener() {
188 | @Override
189 | public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {
190 | if (error instanceof UnrecognizedInputFormatException) {
191 | release();
192 | eventHandler.obtainMessage(3).sendToTarget();
193 | }
194 | }
195 | });
196 | }
197 |
198 | private long getAdjustedSeekPosition(long pos) {
199 | SeekParameters seekParameters = SeekParameters.CLOSEST_SYNC;
200 | return mediaPeriod.getAdjustedSeekPositionUs(pos, seekParameters);
201 | }
202 |
203 | public long getKeyFramePosition(long pos) {
204 | try {
205 | SeekParameters seekParameters = SeekParameters.CLOSEST_SYNC;
206 | return mediaPeriod.getAdjustedSeekPositionUs(pos, seekParameters);
207 | } catch (NullPointerException ex) {
208 | return pos;
209 | }
210 | }
211 |
212 | public long getNextKeyFrame(long pos) {
213 | try {
214 | SeekParameters seekParameters = SeekParameters.NEXT_SYNC;
215 | return mediaPeriod.getAdjustedSeekPositionUs(pos + 100, seekParameters);
216 | } catch (NullPointerException ex) {
217 | return pos;
218 | }
219 | }
220 |
221 | @Override
222 | public void sendMessage(PlayerMessage message) {
223 | defaultHandler.obtainMessage(0, message).sendToTarget();
224 | }
225 |
226 |
227 | public void destroy() {
228 | defaultHandler.sendEmptyMessage(12);
229 | }
230 |
231 | public boolean isAlive() {
232 | return internalPlaybackThread != null && internalPlaybackThread.isAlive();
233 | }
234 |
235 | private void insureWorker() {
236 | if (!isAlive()) {
237 | if (internalPlaybackThread != null) {
238 | internalPlaybackThread.quit();
239 | }
240 | createWorkerThread();
241 | }
242 | }
243 |
244 | private void releaseResourcesInternal() {
245 | if (mediaSource == null) return;
246 | periodPrepared = false;
247 | defaultHandler.removeMessages(2); // -=doSomeWork
248 | defaultHandler.removeMessages(4); // -=continueWork
249 | rendererPositionUs = 0;
250 | try {
251 | if (videoRenderer.getState() == Renderer.STATE_STARTED) {
252 | videoRenderer.stop();
253 | }
254 |
255 | if (videoRenderer.getState() == Renderer.STATE_ENABLED) {
256 | videoRenderer.disable();
257 | }
258 | } catch (ExoPlaybackException e) {
259 |
260 | }
261 | mediaSource.releasePeriod(mediaPeriod);
262 | mediaSource.releaseSource(this);
263 | mediaSource = null;
264 | mediaPeriod = null;
265 | control.onReleased();
266 | }
267 |
268 | @Override
269 | public boolean handleMessage(Message msg) {
270 | Log.d("handl_test", msg.what + " " + (trimInfo != null ? trimInfo.url.substring(trimInfo.url.lastIndexOf("/")) : ""));
271 | try {
272 | switch (msg.what) {
273 | case 0:
274 | PlayerMessage message = (PlayerMessage) msg.obj;
275 | try {
276 | message.getTarget().handleMessage(message.getType(), message.getPayload());
277 | } catch (ExoPlaybackException e) {
278 | e.printStackTrace();
279 | }
280 | break;
281 |
282 | case 1:
283 | this.trimInfo = (TrimInfo) msg.obj;
284 |
285 | if (periodPrepared && mediaPeriod != null) {
286 | handlePeriodPrepared(mediaPeriod);
287 | } else {
288 | prepareInternal();
289 | }
290 | break;
291 |
292 | case 2:
293 | doSomeWork();
294 | break;
295 |
296 | case 3:
297 | try {
298 | handlePeriodPrepared((MediaPeriod) msg.obj);
299 | } catch (ExoPlaybackException e) {
300 | e.printStackTrace();
301 | }
302 |
303 | break;
304 | case 4:
305 | handleContinueLoading();
306 | break;
307 |
308 | case 10:
309 | try {
310 | seekToPeriodPosition((Long) msg.obj);
311 | } catch (ExoPlaybackException e) {
312 | e.printStackTrace();
313 | }
314 | break;
315 |
316 | case 11:
317 | releaseResourcesInternal();
318 | break;
319 |
320 | case 12:
321 | releaseResourcesInternal();
322 | defaultHandler.removeCallbacksAndMessages(null);
323 | internalPlaybackThread.quit();
324 | break;
325 |
326 | case 14:
327 | finishInternal();
328 | break;
329 | }
330 | } catch (Exception e) {
331 | Log.d("ex", e.getMessage());
332 | }
333 | return true;
334 | }
335 |
336 | public void finishInternal() {
337 | defaultHandler.removeMessages(2); // -=doSomeWork
338 | defaultHandler.removeMessages(4); // -=continueWork
339 | }
340 |
341 | public void onFinishedLoading() {
342 | defaultHandler.sendEmptyMessage(14);
343 | }
344 |
345 | private void doSomeWork() {
346 | if (periodPrepared) {
347 | try {
348 | videoRenderer.render(rendererPositionUs, SystemClock.elapsedRealtime() * 1000);
349 | } catch (ExoPlaybackException e) {
350 | e.printStackTrace();
351 | } catch (IllegalStateException e) {
352 | releaseResourcesInternal();
353 | Log.d("Exception", e.getMessage() != null ? e.getMessage() : "exception"); // at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
354 | }
355 |
356 | defaultHandler.sendEmptyMessage(2);
357 | }
358 | }
359 |
360 | private void handleContinueLoading() {
361 | long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
362 | if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
363 | return;
364 | }
365 | long bufferedDurationUs = mediaPeriod.getNextLoadPositionUs();
366 | if (bufferedDurationUs - rendererPositionUs < 2 * C.MICROS_PER_SECOND) {
367 | try {
368 | mediaPeriod.continueLoading(rendererPositionUs);
369 | } catch (Exception e) {
370 |
371 | }
372 | } else {
373 | Log.d("adkjfdkjf", "dkfjakjfd");
374 | }
375 | }
376 |
377 |
378 | private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
379 | pendingSeekPositinoUs = trimInfo.startSeconds * C.MICROS_PER_SECOND;
380 |
381 | if (trimInfo.endSeconds != C.TIME_END_OF_SOURCE) {
382 | pendingEndPositionUs = trimInfo.endSeconds * C.MICROS_PER_SECOND;
383 | } else {
384 | pendingEndPositionUs = C.TIME_END_OF_SOURCE;
385 | }
386 |
387 | if (!periodPrepared) {
388 | TrackGroupArray trackGroups = mediaPeriod.getTrackGroups();
389 | // if (trackSelector.getParameters() != null) {
390 | // DefaultTrackSelector.Parameters parameters =
391 | // trackSelector.getParameters().buildUpon()
392 | // .setForceLowestBitrate(true).build();
393 | // trackSelector.setParameters(parameters);
394 | // }
395 | selectorResult = trackSelector.selectTracks(new RendererCapabilities[]{rendererCapabilities},
396 | trackGroups, new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(0)), timeline);
397 |
398 | updateLoadControlTrackSelection(trackGroups, selectorResult);
399 |
400 | TrackSelectionArray videoSelection = selectorResult.selections;
401 |
402 | SampleStream[] sampleStreams = new SampleStream[1];
403 | mediaPeriod.selectTracks(
404 | videoSelection.getAll(),
405 | new boolean[1],
406 | sampleStreams,
407 | new boolean[1],
408 | 0);
409 |
410 | if (!selectorResult.isRendererEnabled(0)) return;
411 |
412 | associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
413 |
414 | RendererConfiguration rendererConfiguration = selectorResult.rendererConfigurations[0];
415 | Format[] formats = getFormats(videoSelection.get(0));
416 |
417 | videoRenderer.enable(rendererConfiguration, formats, sampleStreams[0], rendererPositionUs, false, 0);
418 |
419 | videoRenderer.start();
420 |
421 | periodPrepared = true;
422 | // eventHandler.obtainMessage(1, extractorsFactory.getMp4Size()).sendToTarget();
423 | }
424 |
425 | if (pendingSeekPositinoUs >= 0) {
426 | seekToPeriodPosition(pendingSeekPositinoUs);
427 | }
428 |
429 | defaultHandler.sendEmptyMessage(2);
430 | }
431 |
432 | private void updateLoadControlTrackSelection(
433 | TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
434 | control.onTracksSelected(new Renderer[]{videoRenderer}, trackGroups, trackSelectorResult.selections);
435 | }
436 |
437 | // seeking
438 |
439 | private long seekToPeriodPosition(long periodPositionUs)
440 | throws ExoPlaybackException {
441 | Log.d("speed_test", "seek to " + periodPositionUs / C.MICROS_PER_SECOND + "s");
442 | // videoRenderer.stop();
443 |
444 | // Update the holders.
445 | if (mediaPeriod != null) {
446 | periodPositionUs = getAdjustedSeekPosition(periodPositionUs);
447 |
448 | this.rendererPositionUs = periodPositionUs;
449 |
450 | periodPositionUs = mediaPeriod.seekToUs(periodPositionUs);
451 | mediaPeriod.discardBuffer(
452 | periodPositionUs, true);
453 |
454 | videoRenderer.resetPosition(periodPositionUs);
455 |
456 | Log.d("killer_john", "seek to position");
457 | defaultHandler.obtainMessage(4).sendToTarget();
458 | }
459 |
460 | return periodPositionUs;
461 | }
462 |
463 | @Override
464 | public void onTrackSelectionsInvalidated() {
465 |
466 | }
467 |
468 | @Override
469 | public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {
470 |
471 | }
472 |
473 | @Override
474 | public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) {
475 |
476 | }
477 |
478 | @Override
479 | public void onBytesTransferred(DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) {
480 |
481 | }
482 |
483 | @Override
484 | public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {
485 |
486 | }
487 |
488 | public void setListener(VideoListener listener) {
489 | this.listener = listener;
490 | }
491 |
492 | @Override
493 | public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
494 | this.timeline = timeline;
495 | }
496 |
497 |
498 | private final class ComponentListener
499 | implements VideoRendererEventListener,
500 | MetadataOutput,
501 | SurfaceHolder.Callback,
502 | TextureView.SurfaceTextureListener
503 | {
504 |
505 | // VideoRendererEventListener implementation
506 |
507 | @Override
508 | public void onVideoEnabled(DecoderCounters counters) {
509 | }
510 |
511 |
512 | @Override
513 | public void onVideoInputFormatChanged(Format format) {
514 | eventHandler.obtainMessage(2, format).sendToTarget();
515 | }
516 |
517 | @Override
518 | public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
519 | float pixelWidthHeightRatio) {
520 | float videoAspect = ((float) width / height) * pixelWidthHeightRatio;
521 | if (listener != null) {
522 | listener.onVideoSizeChanged(width, height,unappliedRotationDegrees, pixelWidthHeightRatio);
523 | }
524 | }
525 |
526 | @Override
527 | public void onRenderedFirstFrame(Surface surface) {
528 |
529 | }
530 |
531 | @Override
532 | public void onVideoDisabled(DecoderCounters counters) {
533 | }
534 |
535 | @Override
536 | public void onMetadata(Metadata metadata) {
537 | // for (MetadataOutput metadataOutput : metadataOutputs) {
538 | // metadataOutput.onMetadata(metadata);
539 | // }
540 | }
541 |
542 | // SurfaceHolder.Callback implementation
543 |
544 | @Override
545 | public void surfaceCreated(SurfaceHolder holder) {
546 | // setVideoSurfaceInternal(holder.getSurface(), false);
547 | }
548 |
549 | @Override
550 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
551 | // maybeNotifySurfaceSizeChanged(width, height);
552 | }
553 |
554 | @Override
555 | public void surfaceDestroyed(SurfaceHolder holder) {
556 | // setVideoSurfaceInternal(null, false);
557 | // maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
558 | }
559 |
560 | // TextureView.SurfaceTextureListener implementation
561 |
562 | @Override
563 | public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
564 | // setVideoSurfaceInternal(new Surface(surfaceTexture), true);
565 | // maybeNotifySurfaceSizeChanged(width, height);
566 | }
567 |
568 | @Override
569 | public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
570 | // maybeNotifySurfaceSizeChanged(width, height);
571 | }
572 |
573 | @Override
574 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
575 | // setVideoSurfaceInternal(null, true);
576 | // maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
577 | return true;
578 | }
579 |
580 | @Override
581 | public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
582 | // Do nothing.
583 | }
584 |
585 | }
586 |
587 | private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
588 | if (rendererCapabilities.getTrackType() == C.TRACK_TYPE_NONE
589 | && selectorResult.isRendererEnabled(0)) {
590 | sampleStreams[0] = new EmptySampleStream();
591 | }
592 | }
593 |
594 | private static Format[] getFormats(TrackSelection newSelection) {
595 | // Build an array of formats contained by the selection.
596 | int length = newSelection != null ? newSelection.length() : 0;
597 | Format[] formats = new Format[length];
598 | for (int i = 0; i < length; i++) {
599 | formats[i] = newSelection.getFormat(i);
600 | }
601 | return formats;
602 | }
603 |
604 | }
605 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/TrimInfo.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | public class TrimInfo {
4 | public TrimInfo(long start, long end, String url) {
5 | this.startSeconds = start;
6 | this.endSeconds = end;
7 | this.url = url;
8 | }
9 | public final long startSeconds;
10 | public final long endSeconds;
11 | public final String url;
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/VideoAdapter.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.content.Context;
4 | import android.media.ThumbnailUtils;
5 | import android.provider.MediaStore;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.ImageView;
9 |
10 | import androidx.annotation.NonNull;
11 | import androidx.recyclerview.widget.RecyclerView;
12 | import androidx.recyclerview.widget.RecyclerView.ViewHolder;
13 |
14 | import java.util.List;
15 |
16 | import bolts.Task;
17 |
18 | public class VideoAdapter extends RecyclerView.Adapter {
19 |
20 | private List data;
21 | private ItemSelectListener itemSelectListener;
22 | private int sizeW;
23 | private int sizeH;
24 |
25 | public VideoAdapter(Context context ,List d, ItemSelectListener itemSelectListener) {
26 | this.data = d;
27 | this.itemSelectListener = itemSelectListener;
28 | this.sizeW = Android.dpToPx(context, 120);
29 | }
30 |
31 | @NonNull
32 | @Override
33 | public VHolder onCreateViewHolder(ViewGroup parent, int viewType) {
34 | ImageView imageView = new ImageView(parent.getContext());
35 | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
36 | imageView.setLayoutParams(new ViewGroup.LayoutParams(sizeW, sizeW));
37 | VHolder holder = new VHolder(imageView);
38 | holder.itemView.setOnClickListener(view -> {
39 | if (itemSelectListener != null) {
40 | itemSelectListener.onSelect(data.get(holder.getAdapterPosition()));
41 | }
42 | });
43 | return holder;
44 | }
45 |
46 | @Override
47 | public void onBindViewHolder(VHolder holder, final int position) {
48 | String path = data.get(holder.getAdapterPosition());
49 |
50 | holder.imageView.setImageBitmap(null);
51 | Task.call(() -> ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND),
52 | Task.BACKGROUND_EXECUTOR).continueWith(task -> {
53 | if (task.getResult() != null && holder.getAdapterPosition() == position) {
54 | holder.imageView.setImageBitmap(task.getResult());
55 | }
56 | return null;
57 | }, Task.UI_THREAD_EXECUTOR);
58 | }
59 |
60 | @Override
61 | public int getItemCount() {
62 | return data.size();
63 | }
64 |
65 | static class VHolder extends ViewHolder {
66 | ImageView imageView;
67 | VHolder(@NonNull View itemView) {
68 | super(itemView);
69 | imageView = (ImageView) itemView;
70 | }
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/VideoFrameAdapter.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.ImageView;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.recyclerview.widget.RecyclerView;
10 |
11 | import com.video.timeline.ImageLoader;
12 | import com.video.timeline.RetroInstance;
13 | import com.video.timeline.VideoMetadata;
14 |
15 | import java.util.List;
16 |
17 | public class VideoFrameAdapter extends RecyclerView.Adapter {
18 | private List mets;
19 | private final ImageLoader imageLoader;
20 | private final long frameDuration;
21 | private Context context;
22 | int count;
23 |
24 | private RetroInstance retroInstance;
25 |
26 | public VideoFrameAdapter(RetroInstance retroInstance, int frameDuration, long videoDuration, List mets, ImageLoader imageLoader) {
27 | this.frameDuration = frameDuration;
28 | this.mets = mets;
29 | this.imageLoader = imageLoader;
30 | count = (int) (videoDuration / frameDuration);
31 |
32 | this.retroInstance = retroInstance;
33 | }
34 | @NonNull
35 | @Override
36 | public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
37 | ImageView imageView = new ImageView(parent.getContext());
38 | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
39 | imageView.setLayoutParams(new ViewGroup.LayoutParams(retroInstance.frameSize(), retroInstance.frameSize()));
40 | return new Holder(imageView);
41 | }
42 |
43 | private long getWindowPosition(long outPosition) {
44 | long len = 0;
45 | for (VideoMetadata metadata: mets) {
46 | if (outPosition <= metadata.getDurationMs() + len) {
47 | return outPosition - len;
48 | }
49 | len += metadata.getDurationMs();
50 | }
51 |
52 | return len;
53 | }
54 |
55 | @Override
56 | public void onBindViewHolder(@NonNull Holder holder, int position) {
57 | ImageView imageView = (ImageView)holder.itemView;
58 | imageView.setImageBitmap(null);
59 | retroInstance.load(hashCode() + "",getWindowPosition(position * frameDuration), holder.hashCode(),
60 | file -> imageLoader.load(file, imageView));
61 | }
62 |
63 | @Override
64 | public int getItemCount() {
65 | return count;
66 | }
67 |
68 | static class Holder extends RecyclerView.ViewHolder {
69 | public Holder(@NonNull View itemView) {
70 | super(itemView);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/timlineview/VideoFrameAdapter2.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.ImageView;
8 |
9 | import androidx.annotation.NonNull;
10 | import androidx.recyclerview.widget.RecyclerView;
11 |
12 | import com.video.timeline.ImageLoader;
13 | import com.video.timeline.RetroInstance;
14 | import com.video.timeline.VideoMetadata;
15 |
16 | import java.util.List;
17 |
18 | public class VideoFrameAdapter2 extends RecyclerView.Adapter {
19 | private final ImageLoader imageLoader;
20 | private List medias;
21 | private List infos;
22 | private final long frameDuration;
23 | private Context context;
24 | int count;
25 |
26 | private RetroInstance retroInstance;
27 |
28 | public VideoFrameAdapter2(RetroInstance retroInstance,
29 | int frameDuration,
30 | ImageLoader imageLoader,
31 | List medias,
32 | List infos) {
33 | this.frameDuration = frameDuration;
34 | this.imageLoader = imageLoader;
35 | this.medias = medias;
36 | this.infos = infos;
37 |
38 | long duration = 0;
39 | for (VideoMetadata info: infos) {
40 | Log.d("2_study", info.getDurationMs() + "");
41 | duration += info.getDurationMs();
42 | }
43 |
44 | count = (int) (duration / frameDuration);
45 |
46 | this.retroInstance = retroInstance;
47 | }
48 |
49 | private String getMediaForPosition(int position) {
50 | int len = 0;
51 | for (VideoMetadata info: infos) {
52 | len += (int) (info.getDurationMs() / frameDuration);
53 | if (position <= len) {
54 | return medias.get(infos.indexOf(info));
55 | }
56 | }
57 |
58 | return medias.get(medias.size() - 1);
59 | }
60 |
61 | private int offset(String media) {
62 | int total = 0;
63 | for (String item: medias) {
64 | if (item == media) {
65 | return total;
66 | }
67 | total += infos.get(medias.indexOf(item)).getDurationMs() / frameDuration;
68 | }
69 |
70 | return total;
71 | }
72 |
73 | @NonNull
74 | @Override
75 | public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
76 | ImageView imageView = new ImageView(parent.getContext());
77 | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
78 | imageView.setLayoutParams(new ViewGroup.LayoutParams(retroInstance.frameSize(), retroInstance.frameSize()));
79 | return new Holder(imageView);
80 | }
81 |
82 | @Override
83 | public void onBindViewHolder(@NonNull Holder holder, int position) {
84 | ImageView imageView = (ImageView)holder.itemView;
85 | imageView.setImageBitmap(null);
86 | String video = getMediaForPosition(position);
87 | retroInstance.load(video,
88 | (position - offset(video)) * frameDuration,
89 | holder.hashCode(),
90 | file -> imageLoader.load(file, imageView));
91 | }
92 |
93 | @Override
94 | public int getItemCount() {
95 | return count;
96 | }
97 |
98 | static class Holder extends RecyclerView.ViewHolder {
99 | public Holder(@NonNull View itemView) {
100 | super(itemView);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
30 |
31 |
32 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preview2.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
16 |
17 |
21 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preview3.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
28 |
29 |
36 |
37 |
44 |
45 |
49 |
50 |
56 |
61 |
62 |
68 |
69 |
74 |
75 |
76 |
81 |
82 |
88 |
89 |
93 |
94 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Timeline Demo
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/timlineview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.example.timlineview;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | repositories {
6 | google()
7 | jcenter()
8 |
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.6.2'
12 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7'
13 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
14 | classpath 'com.novoda:bintray-release:0.9.2'
15 |
16 | // NOTE: Do not place your application dependencies here; they belong
17 | // in the individual module build.gradle files
18 | }
19 | }
20 |
21 | allprojects {
22 | repositories {
23 | google()
24 | jcenter()
25 | }
26 | }
27 |
28 | task clean(type: Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 02 20:06:06 AMT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /src/main/java/com/video/timeline/tests
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'com.novoda.bintray-release'
3 |
4 | publish {
5 | userOrg = 'johnnyd'
6 | groupId = 'com.chemicalbird.android'
7 | artifactId = 'videotimelineview'
8 | uploadName = 'com.video.timeline'
9 | publishVersion = '0.0.4'
10 | desc = 'Video timeline view (Film strip) based on ExoPlayer API'
11 | website = 'https://github.com/chemicalbird/ExoPlayerTimelineView'
12 | }
13 |
14 | android {
15 | compileSdkVersion 29
16 | buildToolsVersion "29.0.3"
17 |
18 | defaultConfig {
19 | minSdkVersion 17
20 | targetSdkVersion 29
21 | versionCode 1
22 | versionName "1.0"
23 |
24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
25 | consumerProguardFiles 'consumer-rules.pro'
26 | }
27 |
28 | buildTypes {
29 | release {
30 | minifyEnabled false
31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
32 | }
33 | }
34 |
35 | compileOptions {
36 | sourceCompatibility 1.8
37 | targetCompatibility 1.8
38 | }
39 |
40 | lintOptions {
41 | abortOnError false
42 | }
43 |
44 |
45 | }
46 |
47 | dependencies {
48 | implementation fileTree(dir: 'libs', include: ['*.jar'])
49 |
50 | implementation 'androidx.appcompat:appcompat:1.1.0'
51 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
52 |
53 | api 'com.google.android.exoplayer:exoplayer-core:2.11.4'
54 | api ('com.google.android.exoplayer:exoplayer-ui:2.11.4') {
55 | exclude group: 'com.android.support'
56 | }
57 |
58 | api 'com.otaliastudios.opengl:egloo:0.4.0'
59 | }
60 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle'
61 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'
62 |
--------------------------------------------------------------------------------
/library/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/library/consumer-rules.pro
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/library/src/androidTest/java/com/video/timeline/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("com.video.timeline.test", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/ExoPlayerFactory.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.content.Context;
4 | import android.net.Uri;
5 |
6 | import com.google.android.exoplayer2.DefaultLoadControl;
7 | import com.google.android.exoplayer2.SeekParameters;
8 | import com.google.android.exoplayer2.SimpleExoPlayer;
9 | import com.google.android.exoplayer2.source.MediaSource;
10 | import com.google.android.exoplayer2.source.ProgressiveMediaSource;
11 | import com.google.android.exoplayer2.upstream.DataSource;
12 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
13 |
14 | public class ExoPlayerFactory {
15 |
16 | private SeekParameters seekParameters;
17 | private boolean preferSoftwareDecoder;
18 | private DataSource.Factory dataSourceFactory;
19 |
20 | ExoPlayerFactory(SeekParameters seekParameters) {
21 | this(seekParameters, false, null);
22 | }
23 |
24 | ExoPlayerFactory(SeekParameters seekParameters,
25 | boolean preferSoftwareDecoder,
26 | DataSource.Factory dataSourceFactory) {
27 | this.seekParameters = seekParameters;
28 | this.preferSoftwareDecoder = preferSoftwareDecoder;
29 | this.dataSourceFactory = dataSourceFactory;
30 | }
31 |
32 | public SimpleExoPlayer getPlayer(Context context) {
33 | int bufferMs = preferSoftwareDecoder ? 100 : 2000;
34 | SimpleExoPlayer exoPlayer = new SimpleExoPlayer.Builder(context, new VideoRendererOnlyFactory(context, preferSoftwareDecoder))
35 | .setLoadControl(
36 | new DefaultLoadControl.Builder()
37 | .setBufferDurationsMs(
38 | bufferMs, bufferMs, 0, 0)
39 | .createDefaultLoadControl()
40 | )
41 | .build();
42 | exoPlayer.setSeekParameters(seekParameters);
43 | return exoPlayer;
44 | }
45 |
46 | public MediaSource getMediaSource(String mediaUri, Context context) {
47 | if (dataSourceFactory == null) {
48 | dataSourceFactory = new DefaultDataSourceFactory(context, "exo");
49 | }
50 | return new ProgressiveMediaSource.Factory(dataSourceFactory)
51 | .createMediaSource(Uri.parse(mediaUri));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/FetchCallback.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | public interface FetchCallback {
4 | void onSuccess(T result);
5 | }
6 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/ImageLoader.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.widget.ImageView;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import java.io.File;
8 |
9 | public interface ImageLoader {
10 | void load(@Nullable File file, ImageView into);
11 | }
12 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/RetroInstance.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.content.Context;
4 | import android.os.Handler;
5 | import android.os.SystemClock;
6 | import android.util.Log;
7 | import android.view.Surface;
8 |
9 | import com.google.android.exoplayer2.ExoPlaybackException;
10 | import com.google.android.exoplayer2.Player;
11 | import com.google.android.exoplayer2.SeekParameters;
12 | import com.google.android.exoplayer2.SimpleExoPlayer;
13 | import com.google.android.exoplayer2.source.MediaSource;
14 | import com.google.android.exoplayer2.upstream.DataSource;
15 | import com.google.android.exoplayer2.video.VideoListener;
16 | import com.video.timeline.android.MRetriever;
17 | import com.video.timeline.render.RetroRenderer;
18 | import com.video.timeline.tools.FileHelper;
19 | import com.video.timeline.tools.Loggy;
20 |
21 | import java.io.File;
22 | import java.nio.ByteBuffer;
23 | import java.util.HashMap;
24 | import java.util.LinkedList;
25 | import java.util.concurrent.Executors;
26 |
27 | public class RetroInstance implements RetroSurfaceListener, VideoListener, Player.EventListener {
28 |
29 | private final int size;
30 | private Context context;
31 |
32 | private RetroRenderer offscreenSurface;
33 | private LinkedList jobs = new LinkedList<>();
34 | private HashMap jobHashMap = new HashMap<>();
35 | private Task currentJob;
36 |
37 | private ExoPlayerFactory videoPlayerFactory;
38 | private VideoFrameCache videoFrameCache;
39 | private SimpleExoPlayer player;
40 | private MediaSource explicitMediaSource;
41 |
42 | private Handler mainHandler = new Handler();
43 | private long playerPosition;
44 | private long lastSeekPos;
45 | private MRetriever mediaMetRetreiver;
46 |
47 | private String currentPreparedSource;
48 | private long time;
49 | private long totalTime;
50 |
51 | private RetroInstance(ExoPlayerFactory playerFactory, int size, Context context, File cacheDir) {
52 | this.size = size;
53 | this.context = context;
54 | this.videoPlayerFactory = playerFactory;
55 | this.videoFrameCache = new VideoFrameCache(cacheDir);
56 | }
57 |
58 | public File getCacheDir() {
59 | return videoFrameCache.getCacheDir();
60 | }
61 |
62 | public void setPlayerInstance(SimpleExoPlayer player) {
63 | this.player = player;
64 | }
65 |
66 | public void setExplicitMediaSource(MediaSource explicitMediaSource) {
67 | this.explicitMediaSource = explicitMediaSource;
68 | if (player != null) {
69 | player.prepare(explicitMediaSource);
70 | }
71 | }
72 |
73 | public void load(String mediaUri, Long presentationMs, int hash, FetchCallback callback) {
74 | Task previous = jobHashMap.get(hash);
75 | if (previous != null) {
76 | jobs.remove(previous);
77 | jobHashMap.remove(hash);
78 | }
79 |
80 | Task job = new Task(mediaUri, presentationMs, hash, callback);
81 |
82 | File cache = findCache(job);
83 | if (cache != null) {
84 | job.callback.onSuccess(cache);
85 | } else {
86 | jobHashMap.put(hash, job);
87 | jobs.add(job);
88 | execute();
89 | }
90 | }
91 |
92 | private File findCache(Task item) {
93 | File cache = videoFrameCache.fileAt(item.mediaUri, item.time);
94 | return cache != null && cache.exists() ? cache : null;
95 | }
96 |
97 | private void execute() {
98 | if (currentJob == null && !jobs.isEmpty()) {
99 | currentJob = jobs.poll();
100 | time = SystemClock.elapsedRealtime();
101 | if (currentJob != null) {
102 | File cache = findCache(currentJob);
103 | if (cache != null) {
104 | Log.d("retro_study", "Cache hit: " + currentJob.time);
105 | currentJob.callback.onSuccess(cache);
106 | jobHashMap.remove(currentJob.hash);
107 | next();
108 | } else if (mediaMetRetreiver != null) { // fallback is activated
109 | loadUsingFallbackMethod();
110 | } else {
111 | checkPlayerAndSurface();
112 | Log.d("retro_study", "Decode: " + currentJob.time);
113 | if (currentJob.time >= 0) {
114 | playerPosition = player.getCurrentPosition();
115 | lastSeekPos = playerPosition == currentJob.time ? currentJob.time + 100L : currentJob.time;
116 | player.seekTo(lastSeekPos);
117 | }
118 | }
119 | } else {
120 | execute();
121 | }
122 | }
123 | }
124 |
125 | private void next() {
126 | currentJob = null;
127 | execute();
128 | }
129 |
130 | private void checkPlayerAndSurface() {
131 | if (player == null) {
132 | prepareVideoPlayer();
133 | }
134 | if (explicitMediaSource != null) {
135 | if (isIdle()) {
136 | player.prepare(explicitMediaSource);
137 | }
138 | } else if (!currentJob.mediaUri.equals(currentPreparedSource) || isIdle()) {
139 | player.prepare(videoPlayerFactory.getMediaSource(currentJob.mediaUri, context));
140 | currentPreparedSource = currentJob.mediaUri;
141 | if (offscreenSurface != null) {
142 | offscreenSurface.clearTxtMtx();
143 | }
144 | }
145 | if (offscreenSurface == null) {
146 | offscreenSurface = new RetroRenderer(size, size, this);
147 | }
148 | }
149 |
150 | private void prepareVideoPlayer() {
151 | player = videoPlayerFactory.getPlayer(context);
152 | player.addVideoListener(this);
153 | player.addListener(this);
154 | }
155 |
156 | private boolean isIdle() {
157 | return player.getPlaybackState() == Player.STATE_IDLE;
158 | }
159 |
160 | public void stop(boolean reset) {
161 | if (player != null) {
162 | player.stop(reset);
163 | }
164 | }
165 |
166 | @Override
167 | public void onPlayerError(ExoPlaybackException error) {
168 | Loggy.d("Player err: " + (error != null ? error.getMessage() : ""));
169 | stop(true);
170 | if (error != null && error.type == ExoPlaybackException.TYPE_SOURCE) {
171 | currentJobFinished();
172 | return;
173 | }
174 |
175 | if (explicitMediaSource == null) { // fallback not handled for explicit mediaSource
176 | if (mediaMetRetreiver == null) {
177 | mediaMetRetreiver = new MRetriever(context, frameSize(),
178 | Executors.newFixedThreadPool(1));
179 | }
180 | loadUsingFallbackMethod();
181 | }
182 | }
183 |
184 | private void loadUsingFallbackMethod() {
185 | if (currentJob != null) {
186 | mediaMetRetreiver.frameAt(currentJob.mediaUri, currentJob.time, result -> {
187 | File cacheWrite = videoFrameCache.fileAt(currentJob.mediaUri, currentJob.time);
188 | if (cacheWrite != null && result != null) {
189 | FileHelper.saveBitmapToFile(cacheWrite, result);
190 | result.recycle();
191 | }
192 | mainHandler.post(this::currentJobFinished);
193 | }, currentJob.hash);
194 | }
195 | }
196 |
197 | @Override
198 | public void onSurfaceAvailable(Surface surface) {
199 | mainHandler.post(() -> this.player.setVideoSurface(surface));
200 | }
201 |
202 | @Override
203 | public void onTextureRetrieved(ByteBuffer pixelBuffer) {
204 | if (currentJob == null) return; // ?investigate
205 |
206 | totalTime += (SystemClock.elapsedRealtime() - time);
207 | File cacheWrite = videoFrameCache.fileAt(currentJob.mediaUri, currentJob.time);
208 | if (cacheWrite != null) {
209 | FileHelper.saveToFile(cacheWrite, pixelBuffer, size, size);
210 | }
211 | mainHandler.post(new Runnable() {
212 | @Override
213 | public void run() {
214 | currentJobFinished();
215 | }
216 | });
217 | }
218 |
219 | private void currentJobFinished() {
220 | if (currentJob != null) {
221 | currentJob.callback.onSuccess(videoFrameCache.fileAt(currentJob.mediaUri, currentJob.time));
222 | jobHashMap.remove(currentJob.hash);
223 | }
224 | next();
225 | }
226 |
227 | @Override
228 | public void onSeekProcessed() {
229 | if (currentJob == null) return;
230 | if (player != null && player.getDuration() > 0) {
231 | if ((lastSeekPos != 0 && player.getCurrentPosition() == playerPosition) || lastSeekPos > player.getDuration()) {
232 | offscreenSurface.drawSameFrame();
233 | }
234 | }
235 | }
236 |
237 | @Override
238 | public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
239 | float videoAspect = ((float) width / height) * pixelWidthHeightRatio;
240 | offscreenSurface.onVideoAspectChanged(videoAspect);
241 | }
242 |
243 | public void onDestroy() {
244 | jobs.clear();
245 | jobHashMap.clear();
246 | currentJob = null;
247 | if (player != null) {
248 | player.removeListener(this);
249 | player.removeVideoListener(this);
250 | player.release();
251 | player = null;
252 | }
253 |
254 | if (offscreenSurface != null) {
255 | offscreenSurface.release();
256 | offscreenSurface = null;
257 | }
258 |
259 | if (mediaMetRetreiver != null) {
260 | mediaMetRetreiver.release();
261 | mediaMetRetreiver = null;
262 | }
263 | }
264 |
265 | public int frameSize() {
266 | return size;
267 | }
268 |
269 | public static class Builder {
270 | private static final int DEFAULT_SIZE = 80;
271 |
272 | private Context context;
273 | private int size;
274 | private SeekParameters seekParams;
275 | private File cacheDir;
276 | private boolean preferSoftwareDecoder;
277 | private DataSource.Factory dataSourceFactory;
278 |
279 | public Builder(Context context) {
280 | this.context = context;
281 | this.size = DEFAULT_SIZE;
282 | seekParams = SeekParameters.CLOSEST_SYNC;
283 | cacheDir = new File(context.getCacheDir(), ".frames");
284 | }
285 |
286 | public Builder setSeekParams(SeekParameters seekParams) {
287 | this.seekParams = seekParams;
288 | return this;
289 | }
290 |
291 | public Builder softwareDecoder(boolean flag) {
292 | this.preferSoftwareDecoder = flag;
293 | return this;
294 | }
295 |
296 | public Builder setFrameSizeDp(int size) {
297 | this.size = size;
298 | return this;
299 | }
300 |
301 | public Builder cache(File dir) {
302 | this.cacheDir = dir;
303 | return this;
304 | }
305 |
306 | public Builder sourceFactory(DataSource.Factory factory) {
307 | this.dataSourceFactory = factory;
308 | return this;
309 | }
310 |
311 | public RetroInstance create() {
312 | return new RetroInstance(new ExoPlayerFactory(seekParams, preferSoftwareDecoder, dataSourceFactory),
313 | size, context, cacheDir);
314 | }
315 | }
316 |
317 | static class Task {
318 | private String mediaUri;
319 | final long time;
320 | private int hash;
321 | final FetchCallback callback;
322 |
323 | public Task(String mediaUri, long time, int hash, FetchCallback callback) {
324 | this.mediaUri = mediaUri;
325 | this.time = time;
326 | this.hash = hash;
327 | this.callback = callback;
328 | }
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/RetroSurfaceListener.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.view.Surface;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | public interface RetroSurfaceListener {
8 | void onSurfaceAvailable(Surface surface);
9 | void onTextureRetrieved(ByteBuffer pixelBuffer);
10 | }
11 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/TimelineViewFace.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.content.Context;
4 |
5 | public interface TimelineViewFace {
6 | void startSurfaceRenderer();
7 | void releaseSurface();
8 | Context context();
9 | void attachVideoFactory(ExoPlayerFactory playerFactory);
10 | void setMediaUri(String mediaUri);
11 | }
12 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/VideoFrameCache.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import com.video.timeline.tools.FileHelper;
6 |
7 | import java.io.File;
8 |
9 | public class VideoFrameCache {
10 |
11 | private File cacheDir;
12 |
13 | VideoFrameCache(File cacheDir) {
14 | this.cacheDir = cacheDir;
15 | if (cacheDir != null) {
16 | cacheDir.mkdir();
17 | }
18 | }
19 |
20 | public File getCacheDir() {
21 | return cacheDir;
22 | }
23 |
24 | private String mediaId(String media) {
25 | return media.substring(media.lastIndexOf('/') + 1);
26 | }
27 |
28 | @Nullable
29 | File fileAt(String video, long timeMs) {
30 | return cacheDir != null ? FileHelper.getCachedFile(cacheDir, mediaId(video), timeMs) : null;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/VideoMetadata.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | public class VideoMetadata {
4 | private long durationMs;
5 | private int width;
6 | private int height;
7 |
8 | public float aspectRatio() {
9 | return height == 0 ? 0 : width * 1F / height;
10 | }
11 |
12 | public long getDurationMs() {
13 | return durationMs;
14 | }
15 |
16 | public boolean isCorrupt() {
17 | return durationMs <= 0 || width <=0 || height <= 0;
18 | }
19 |
20 | public boolean isCorruptDimensions() {
21 | return width <=0 || height <= 0;
22 | }
23 |
24 | public void setHeight(int height) {
25 | this.height = height;
26 | }
27 |
28 | public void setWidth(int width) {
29 | this.width = width;
30 | }
31 |
32 | public void setDurationMs(long durationMs) {
33 | this.durationMs = durationMs;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/VideoRendererOnlyFactory.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 |
4 | import android.content.Context;
5 | import android.os.Handler;
6 | import androidx.annotation.Nullable;
7 |
8 | import com.google.android.exoplayer2.DefaultRenderersFactory;
9 | import com.google.android.exoplayer2.Renderer;
10 | import com.google.android.exoplayer2.audio.AudioRendererEventListener;
11 | import com.google.android.exoplayer2.drm.DrmSessionManager;
12 | import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
13 | import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
14 | import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
15 | import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
16 | import com.google.android.exoplayer2.metadata.MetadataOutput;
17 | import com.google.android.exoplayer2.text.TextOutput;
18 | import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
19 | import com.google.android.exoplayer2.video.VideoRendererEventListener;
20 |
21 | import org.jetbrains.annotations.NotNull;
22 |
23 | import java.util.ArrayList;
24 | import java.util.Collections;
25 | import java.util.Comparator;
26 | import java.util.List;
27 |
28 | public class VideoRendererOnlyFactory extends DefaultRenderersFactory {
29 | private Context context;
30 | private boolean preferSoftware;
31 | private Renderer mediaCodecVideoRenderer;
32 |
33 | private MediaCodecSelector selector = new MediaCodecSelector() {
34 | @NotNull
35 | @Override
36 | public List getDecoderInfos(@NotNull String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) throws MediaCodecUtil.DecoderQueryException {
37 | List codecs = MediaCodecUtil.getDecoderInfos(
38 | mimeType, requiresSecureDecoder, requiresTunnelingDecoder);
39 | return preferSoftware ? getDecodersSortedSoftwareFirst(codecs) : codecs;
40 | }
41 |
42 | public List getDecodersSortedSoftwareFirst(
43 | List decoderInfos) {
44 | decoderInfos = new ArrayList<>(decoderInfos);
45 | Collections.sort(decoderInfos, new Comparator() {
46 | @Override
47 | public int compare(MediaCodecInfo o1, MediaCodecInfo o2) {
48 | if (o1.softwareOnly && !o2.softwareOnly) {
49 | return -1;
50 | } else if (o2.softwareOnly && !o1.softwareOnly) {
51 | return 1;
52 | }
53 | return 0;
54 | }
55 | });
56 | return decoderInfos;
57 |
58 | }
59 |
60 | @Nullable
61 | @Override
62 | public MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException {
63 | return MediaCodecUtil.getPassthroughDecoderInfo();
64 | }
65 | };
66 |
67 | public VideoRendererOnlyFactory(Context context, boolean preferSoftware) {
68 | super(context);
69 | this.context = context;
70 | this.preferSoftware = preferSoftware;
71 | }
72 |
73 | @NotNull
74 | @Override
75 | public Renderer[] createRenderers(Handler eventHandler, @NotNull VideoRendererEventListener videoRendererEventListener,
76 | AudioRendererEventListener audioRendererEventListener,
77 | TextOutput textRendererOutput, @NotNull MetadataOutput metadataRendererOutput,
78 | @Nullable DrmSessionManager drmSessionManager) {
79 | ArrayList rendererList = new ArrayList<>();
80 |
81 | buildVideoRenderers(context, EXTENSION_RENDERER_MODE_OFF,
82 | selector, drmSessionManager, false, true,
83 | eventHandler,videoRendererEventListener, 1000, rendererList);
84 | if (rendererList.get(0) instanceof MediaCodecVideoRenderer) {
85 | mediaCodecVideoRenderer = rendererList.get(0);
86 | }
87 | return rendererList.toArray(new Renderer[0]);
88 | }
89 |
90 | public Renderer getMediaCodecVideoRenderer() {
91 | return mediaCodecVideoRenderer;
92 | }
93 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/VideoTimeLine.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline;
2 |
3 | import android.content.Context;
4 | import android.util.DisplayMetrics;
5 | import android.util.TypedValue;
6 |
7 | import com.google.android.exoplayer2.SeekParameters;
8 |
9 | import static android.util.TypedValue.COMPLEX_UNIT_DIP;
10 |
11 | public class VideoTimeLine {
12 |
13 | private TimelineViewFace timelineView;
14 | private boolean started;
15 |
16 | public void start() {
17 | if (started) return;
18 |
19 | started = true;
20 | timelineView.startSurfaceRenderer();
21 | }
22 |
23 | public void destroy() {
24 | release();
25 | }
26 |
27 | private void release() {
28 | timelineView.releaseSurface();
29 | }
30 |
31 | public static Builder with(String mediaUri) {
32 | return new Builder(mediaUri);
33 | }
34 |
35 | public static class Builder {
36 | private final String mediaURI;
37 |
38 | Builder(String mediaURI) {
39 | this.mediaURI = mediaURI;
40 | }
41 |
42 | public VideoTimeLine into(TimelineViewFace fixedView) {
43 | VideoTimeLine timeline = new VideoTimeLine();
44 | timeline.timelineView = fixedView;
45 | timeline.timelineView.setMediaUri(mediaURI);
46 | timeline.timelineView.attachVideoFactory(new ExoPlayerFactory(SeekParameters.NEXT_SYNC));
47 | return timeline;
48 | }
49 |
50 | public VideoTimeLine show(TimelineViewFace view) {
51 | VideoTimeLine timeLine = into(view);
52 | timeLine.start();
53 | return timeLine;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/android/MRetriever.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.android;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.media.MediaMetadataRetriever;
6 | import android.net.Uri;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | import com.video.timeline.FetchCallback;
11 |
12 | import java.util.HashMap;
13 | import java.util.concurrent.ExecutorService;
14 | import java.util.concurrent.Future;
15 |
16 | public class MRetriever {
17 | private final MediaMetadataRetriever mediaMetadataRetriever;
18 | private Context context;
19 | private final int size;
20 | private final ExecutorService threadPoolExecutor;
21 |
22 | private HashMap tasks = new HashMap<>();
23 |
24 | private String currentPreparedSource;
25 |
26 | public MRetriever(Context context, int desiredSize, @NonNull ExecutorService executor) {
27 | this.context = context;
28 | this.size = desiredSize;
29 | mediaMetadataRetriever = new MediaMetadataRetriever();
30 |
31 | threadPoolExecutor = executor;
32 | }
33 |
34 | public void frameAt(String source, long timeMs, FetchCallback fetchCallback, int hashcode) {
35 |
36 | Future task = tasks.get(hashcode);
37 | if (task != null) {
38 | task.cancel(false);
39 | }
40 |
41 | Future future = threadPoolExecutor.submit(() -> {
42 | if (!source.equals(currentPreparedSource)) {
43 | mediaMetadataRetriever.setDataSource(context, Uri.parse(source));
44 | }
45 | fetchCallback.onSuccess(getScaledFrameAt(timeMs * 1000L, MediaMetadataRetriever.OPTION_CLOSEST_SYNC));
46 | currentPreparedSource = source;
47 | });
48 |
49 | tasks.put(hashcode, future);
50 | }
51 |
52 | public void setSource(String media) {
53 | mediaMetadataRetriever.setDataSource(context, Uri.parse(media));
54 | }
55 |
56 | Bitmap getScaledFrameAt(long time, int option) {
57 | Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(time, option);
58 |
59 | try {
60 | int targetWidth;
61 | int targetHeight;
62 | if (bitmap.getHeight() > bitmap.getWidth()) {
63 | targetHeight = size;
64 | float percentage = size * 1F / bitmap.getHeight();
65 | targetWidth = (int) (bitmap.getWidth() * percentage);
66 | } else {
67 | targetWidth = size;
68 | float percentage = size * 1F / bitmap.getWidth();
69 | targetHeight = (int) (bitmap.getHeight() * percentage);
70 | }
71 | Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false);
72 | bitmap.recycle();
73 | return scaledBitmap;
74 | } catch (Exception e) {
75 | e.printStackTrace();
76 | }
77 |
78 | return bitmap;
79 | }
80 |
81 | public void release() {
82 | mediaMetadataRetriever.release();
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/android/MediaRetrieverAdapter.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.android;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.media.MediaMetadataRetriever;
6 | import android.net.Uri;
7 | import android.os.Handler;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.ImageView;
11 |
12 | import androidx.annotation.NonNull;
13 | import androidx.recyclerview.widget.RecyclerView;
14 |
15 | import com.video.timeline.tools.FileHelper;
16 | import com.video.timeline.ImageLoader;
17 | import com.video.timeline.tools.Loggy;
18 | import com.video.timeline.tools.MediaHelper;
19 | import com.video.timeline.VideoMetadata;
20 |
21 | import java.io.File;
22 | import java.util.HashMap;
23 | import java.util.concurrent.ExecutorService;
24 | import java.util.concurrent.Executors;
25 | import java.util.concurrent.Future;
26 |
27 | public class MediaRetrieverAdapter extends RecyclerView.Adapter {
28 | private int frameDuration;
29 | private final int frameSize;
30 | private final ImageLoader imageLoader;
31 | private Context context;
32 | private final String mediaUri;
33 | private final String mediaId;
34 | VideoMetadata videoMetadata;
35 | int count;
36 | private final File cacheDir;
37 |
38 | private Handler handler = new Handler();
39 |
40 | private MRetriever mediaMetadataRetriever;
41 |
42 | private ExecutorService threadPoolExecutor;
43 | private HashMap tasks = new HashMap<>();
44 |
45 |
46 | public MediaRetrieverAdapter(Context c, String mediaUri,
47 | int frameDuration, int frameSize, ImageLoader imageLoader) {
48 | context = c;
49 | this.mediaUri = mediaUri;
50 | this.frameDuration = frameDuration;
51 | this.frameSize = frameSize;
52 | this.imageLoader = imageLoader;
53 |
54 | videoMetadata = new VideoMetadata();
55 | MediaHelper.getVideoMets(c, mediaUri, videoMetadata);
56 | count = (int) (videoMetadata.getDurationMs() / frameDuration);
57 | mediaId = mediaUri.substring(mediaUri.lastIndexOf('/') + 1);
58 |
59 | cacheDir = new File(c.getCacheDir(), ".thumbs");
60 | cacheDir.mkdir();
61 |
62 | threadPoolExecutor = Executors.newFixedThreadPool(1);
63 |
64 | mediaMetadataRetriever = new MRetriever(context, frameSize, Executors.newFixedThreadPool(1));
65 | mediaMetadataRetriever.setSource(mediaUri);
66 | }
67 |
68 | private int getIdentifier(int index) {
69 | return (int) (videoMetadata.getDurationMs() * index / count);
70 | }
71 |
72 | @NonNull
73 | @Override
74 | public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
75 | ImageView imageView = new ImageView(parent.getContext());
76 | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
77 | imageView.setLayoutParams(new ViewGroup.LayoutParams(frameSize, frameSize));
78 | return new Holder(imageView);
79 | }
80 |
81 | @Override
82 | public void onBindViewHolder(@NonNull Holder holder, int position) {
83 | Future task = tasks.get(holder.hashCode());
84 | if (task != null) {
85 | task.cancel(false);
86 | }
87 | ImageView imageView = (ImageView)holder.itemView;
88 | File cache = FileHelper.getCachedFile(cacheDir, mediaId, getIdentifier(position));
89 | imageLoader.load(cache, imageView);
90 |
91 | if (!cache.exists()) {
92 | Future future = threadPoolExecutor.submit(() -> {
93 | Loggy.d("Frame At: " + position * frameDuration);
94 | Bitmap frame = mediaMetadataRetriever.getScaledFrameAt(position * frameDuration * 1000L, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
95 | if (frame != null) {
96 | FileHelper.saveBitmapToFile(cache, frame);
97 | frame.recycle();
98 | handler.post(() -> notifyItemChanged(position));
99 | }
100 | });
101 | tasks.put(holder.hashCode(), future);
102 | } else {
103 | // imageLoader.load(cache.getAbsolutePath(), imageView);
104 | }
105 | }
106 |
107 |
108 | @Override
109 | public int getItemCount() {
110 | return count;
111 | }
112 |
113 | static class Holder extends RecyclerView.ViewHolder {
114 |
115 | public Holder(@NonNull View itemView) {
116 | super(itemView);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/BaseGLRenderer.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.opengl.GLES20;
4 | import android.os.Handler;
5 | import android.os.HandlerThread;
6 | import android.os.Message;
7 | import android.util.Log;
8 |
9 | import androidx.annotation.NonNull;
10 |
11 | import com.otaliastudios.opengl.core.EglCore;
12 | import com.otaliastudios.opengl.core.Egloo;
13 | import com.otaliastudios.opengl.surface.EglSurface;
14 |
15 | import java.io.IOException;
16 | import java.nio.ByteBuffer;
17 | import java.nio.ByteOrder;
18 |
19 | public abstract class BaseGLRenderer implements Handler.Callback {
20 | private final Handler drawHandler;
21 |
22 | private EglCore eglCore;
23 | private EglSurface eglSurface;
24 |
25 | BaseGLRenderer() {
26 | HandlerThread handlerThread = new HandlerThread("offscreen_surface_drawer");
27 | handlerThread.start();
28 | drawHandler = new Handler(handlerThread.getLooper(), this);
29 | drawHandler.obtainMessage(0).sendToTarget();
30 | }
31 |
32 | void requestRender(int itemIndex) {
33 | drawHandler.obtainMessage(2, itemIndex).sendToTarget();
34 | }
35 |
36 | void makeCurrent() {
37 | if (!eglSurface.isCurrent()) {
38 | eglSurface.makeCurrent();
39 | }
40 | }
41 |
42 | public void release() {
43 | drawHandler.obtainMessage(3).sendToTarget();
44 | }
45 |
46 | @Override
47 | public boolean handleMessage(@NonNull Message msg) {
48 | switch (msg.what) {
49 | case 0: // initialize
50 | this.eglCore = new EglCore();
51 | this.eglSurface = createEglSurface(eglCore);
52 |
53 | makeCurrent();
54 | setupDrawingResources(eglCore);
55 | break;
56 |
57 | case 2: // draw
58 | drawFrame((Integer) msg.obj);
59 | break;
60 |
61 | case 3: // destroy
62 | // eglSurface.makeNothingCurrent();
63 | eglSurface.release();
64 | eglCore.release();
65 | break;
66 | }
67 | return false;
68 | }
69 |
70 | abstract EglSurface createEglSurface(EglCore egl);
71 | abstract void setupDrawingResources(EglCore egl);
72 | abstract void drawFrame(int itemId);
73 | }
74 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/FBOHandler.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.opengl.GLES20;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import com.otaliastudios.opengl.draw.GlDrawable;
8 | import com.otaliastudios.opengl.draw.GlRect;
9 | import com.otaliastudios.opengl.texture.GlFramebuffer;
10 | import com.otaliastudios.opengl.texture.GlTexture;
11 |
12 | import static android.opengl.GLES20.GL_FRAMEBUFFER;
13 | import static android.opengl.GLES20.GL_FRAMEBUFFER_BINDING;
14 | import static android.opengl.GLES20.GL_RENDERBUFFER;
15 | import static android.opengl.GLES20.GL_RENDERBUFFER_BINDING;
16 | import static android.opengl.GLES20.GL_TEXTURE_2D;
17 | import static android.opengl.GLES20.GL_TEXTURE_BINDING_2D;
18 |
19 | public class FBOHandler {
20 |
21 | private GlFramebuffer glFramebuffer;
22 | private TextureProgram glProgram;
23 |
24 | public FBOHandler(@NonNull TextureProgram glProgram) {
25 | this.glProgram = glProgram;
26 | initialize();
27 | }
28 |
29 | private void initialize() {
30 | if (glFramebuffer == null) {
31 | glFramebuffer = new GlFramebuffer();
32 | }
33 | }
34 |
35 | public GlTexture configure(int width, int height) {
36 | final int[] args = new int[1];
37 |
38 | GLES20.glGetIntegerv(GL_FRAMEBUFFER_BINDING, args, 0);
39 | final int saveFramebuffer = args[0];
40 | GLES20.glGetIntegerv(GL_RENDERBUFFER_BINDING, args, 0);
41 | final int saveRenderbuffer = args[0];
42 | GLES20.glGetIntegerv(GL_TEXTURE_BINDING_2D, args, 0);
43 | final int saveTexName = args[0];
44 |
45 | GlTexture frameBufferTexture = new GlTexture(GLES20.GL_TEXTURE0, GL_TEXTURE_2D, width, height, GLES20.GL_RGBA);
46 | frameBufferTexture.bind();
47 | glFramebuffer.attach(frameBufferTexture);
48 | frameBufferTexture.unbind();
49 |
50 | GLES20.glBindFramebuffer(GL_FRAMEBUFFER, saveFramebuffer);
51 | GLES20.glBindRenderbuffer(GL_RENDERBUFFER, saveRenderbuffer);
52 | GLES20.glBindTexture(GL_TEXTURE_2D, saveTexName);
53 |
54 | return frameBufferTexture;
55 | }
56 |
57 | float[] getTextureTransform() {
58 | return glProgram.getTextureTransform();
59 | }
60 |
61 | public void draw(GlRect drawable, float[] mvp, float[] tex) {
62 | glFramebuffer.bind();
63 | glProgram.setTextureTransform(tex);
64 | glProgram.draw(drawable, mvp);
65 | }
66 |
67 | void bind() {
68 | glFramebuffer.bind();
69 | }
70 |
71 | public void draw(GlDrawable drawable, float[] mvp) {
72 | glProgram.draw(drawable, mvp);
73 | }
74 |
75 | public void unbind() {
76 | glFramebuffer.unbind();
77 | }
78 |
79 | public void release() {
80 | if (glFramebuffer != null) {
81 | glFramebuffer.release();
82 | }
83 |
84 | if (glProgram != null) {
85 | glProgram.release();
86 | }
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/GlRenderer.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.graphics.SurfaceTexture;
4 | import android.opengl.GLES20;
5 | import android.opengl.GLSurfaceView;
6 | import android.opengl.Matrix;
7 | import android.util.Log;
8 | import android.view.Surface;
9 |
10 | import com.otaliastudios.opengl.draw.GlDrawable;
11 | import com.otaliastudios.opengl.draw.GlRect;
12 | import com.otaliastudios.opengl.texture.GlTexture;
13 | import com.video.timeline.tools.Loggy;
14 |
15 | import javax.microedition.khronos.egl.EGLConfig;
16 | import javax.microedition.khronos.opengles.GL10;
17 |
18 | import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
19 | import static android.opengl.GLES20.GL_FRAMEBUFFER;
20 |
21 | public class GlRenderer implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
22 |
23 | private static final long TIMEOUT = 1000;
24 |
25 | private TextureProgram sceneProgram;
26 | private GlDrawable drawable;
27 | private SurfaceTexture surfaceTexture;
28 |
29 | private final SurfaceEventListener surfaceEventListener;
30 | private int width;
31 | private int height;
32 | private float[] MVPMatrix = new float[16];
33 | private float[] PMatrix = new float[16];
34 | private float[] VMatrix = new float[16];
35 |
36 | private int pendingIndex;
37 | private int frameCount;
38 |
39 | private boolean cleanScene = true;
40 | private boolean startRendering;
41 | private FBOHandler fboHandler;
42 |
43 | private final Object countAvailableLock = new Object();
44 |
45 | GlRenderer(SurfaceEventListener surfaceEventListener) {
46 | this.surfaceEventListener = surfaceEventListener;
47 | Matrix.setLookAtM(VMatrix, 0,
48 | 0.0f, 0.0f, 1.0f,
49 | 0.0f, 0.0f, 0.0f,
50 | 0.0f, 1.0f, 0.0f
51 | );
52 | Matrix.setIdentityM(PMatrix, 0);
53 | }
54 |
55 | @Override
56 | public void onSurfaceCreated(GL10 gl, EGLConfig config) {
57 | createDrawingTools();
58 | }
59 |
60 | private void createDrawingTools() {
61 | if (!startRendering || fboHandler != null) return;
62 | drawable = new GlRect();
63 | sceneProgram = new TextureProgram(TextureProgram.SIMPLE_VERTEX_SHADER, TextureProgram.SIMPLE_FRAGMENT_SHADER);
64 |
65 | GlTexture glTexture = new GlTexture();
66 | fboHandler = new FBOHandler(TextureProgram.createOESProgram(glTexture));
67 |
68 | surfaceTexture = new SurfaceTexture(glTexture.getId());
69 | surfaceTexture.setOnFrameAvailableListener(this);
70 | if (surfaceEventListener != null) {
71 | surfaceEventListener.onSurfaceAvailable(new Surface(surfaceTexture));
72 | } else {
73 | Log.e("error", "Surface is not attached to the player");
74 | }
75 |
76 | Matrix.multiplyMM(MVPMatrix, 0, PMatrix, 0, VMatrix, 0);
77 | }
78 |
79 | @Override
80 | public void onSurfaceChanged(GL10 gl, int width, int height) {
81 | if (this.width != width && this.height != height) {
82 | this.width = width;
83 | this.height = height;
84 |
85 | configureFBO();
86 | }
87 | }
88 |
89 | void setStartRendering() {
90 | startRendering = true;
91 | }
92 |
93 | private void configureFBO() {
94 | if (!startRendering) return;
95 | GlTexture fbTexture = fboHandler.configure(width, height);
96 | sceneProgram.setTexture(fbTexture);
97 | }
98 |
99 | @Override
100 | public void onDrawFrame(GL10 gl) {
101 | if (cleanScene) {
102 | GLES20.glClear(GL_COLOR_BUFFER_BIT);
103 | cleanScene = false;
104 | } else if (pendingIndex == frameCount) {
105 | GLES20.glBindFramebuffer(GL_FRAMEBUFFER, 0);
106 | GLES20.glViewport(0, 0, width, height);
107 | GLES20.glClear(GL_COLOR_BUFFER_BIT);
108 | sceneProgram.draw(drawable);
109 | } else {
110 | surfaceTexture.updateTexImage();
111 | surfaceTexture.getTransformMatrix(fboHandler.getTextureTransform());
112 |
113 | fboHandler.bind();
114 | int size = width / frameCount;
115 | GLES20.glViewport(pendingIndex++ * size, 0, size, height);
116 | fboHandler.draw(drawable, MVPMatrix);
117 | fboHandler.unbind();
118 |
119 | GLES20.glBindFramebuffer(GL_FRAMEBUFFER, 0);
120 | GLES20.glViewport(0, 0, width, height);
121 | GLES20.glClear(GL_COLOR_BUFFER_BIT);
122 |
123 | sceneProgram.draw(drawable);
124 | }
125 | }
126 |
127 | public void release() {
128 | if (fboHandler != null) {
129 | fboHandler.release();
130 | }
131 | if (sceneProgram != null) {
132 | sceneProgram.release();
133 | }
134 | }
135 |
136 | void onAspectPrepared(float videoAspect) {
137 | synchronized (countAvailableLock) {
138 | frameCount = (int) Math.ceil(width * 1f / height);
139 |
140 | if (videoAspect > 1) {
141 | Matrix.orthoM(PMatrix, 0, -1 / videoAspect, 1 / videoAspect, -1, 1, -1, 1);
142 | } else {
143 | Matrix.orthoM(PMatrix, 0, -1, 1, -videoAspect, videoAspect, -1, 1);
144 | }
145 | Matrix.multiplyMM(MVPMatrix, 0, PMatrix, 0, VMatrix, 0);
146 |
147 | countAvailableLock.notifyAll();
148 | }
149 | }
150 |
151 | private void waitForCount() {
152 | synchronized (countAvailableLock) {
153 | if (frameCount == 0) {
154 | try {
155 | Loggy.d("Waiting for clue");
156 | countAvailableLock.wait(TIMEOUT);
157 | if (frameCount == 0) {
158 | Loggy.d("First frame could be damaged");
159 | }
160 | } catch (InterruptedException e) {
161 | e.printStackTrace();
162 | }
163 | }
164 | }
165 | }
166 |
167 | @Override
168 | public void onFrameAvailable(SurfaceTexture surfaceTexture) {
169 | waitForCount();
170 | if (surfaceEventListener != null) {
171 | if (pendingIndex < frameCount) {
172 | surfaceEventListener.nextFrame(pendingIndex, frameCount);
173 | } else {
174 | surfaceEventListener.onFinish();
175 | surfaceTexture.setOnFrameAvailableListener(null);
176 | }
177 | }
178 | }
179 |
180 | public interface SurfaceEventListener{
181 | void onSurfaceAvailable(Surface surface);
182 | void nextFrame(int offset, int limit);
183 | void onFinish();
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/RetroRenderer.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.graphics.SurfaceTexture;
4 | import android.opengl.GLES20;
5 | import android.opengl.Matrix;
6 | import android.view.Surface;
7 |
8 | import com.otaliastudios.opengl.core.EglCore;
9 | import com.otaliastudios.opengl.core.Egloo;
10 | import com.otaliastudios.opengl.draw.GlRect;
11 | import com.otaliastudios.opengl.surface.EglOffscreenSurface;
12 | import com.otaliastudios.opengl.surface.EglSurface;
13 | import com.otaliastudios.opengl.texture.GlTexture;
14 | import com.video.timeline.tools.GlUtils;
15 | import com.video.timeline.tools.Loggy;
16 | import com.video.timeline.RetroSurfaceListener;
17 | import com.video.timeline.VideoFrameCache;
18 |
19 | import java.nio.ByteBuffer;
20 | import java.nio.ByteOrder;
21 |
22 | public class RetroRenderer extends BaseGLRenderer implements SurfaceTexture.OnFrameAvailableListener {
23 |
24 | private static final long TIMEOUT = 5000;
25 |
26 | private float[] mvp;
27 | private float[] projection = new float[16];
28 | private float[] view = new float[16];
29 |
30 | private SurfaceTexture surfaceTexture;
31 | private final GlRect drawable = new GlRect();
32 |
33 | private final RetroSurfaceListener eventListener;
34 | private VideoFrameCache videoCache;
35 | private int mWidth;
36 | private int mHeight;
37 |
38 | private final Object sizeAvailableLock = new Object();
39 |
40 | private FBOHandler fboHandler;
41 |
42 | public RetroRenderer(int width, int height, RetroSurfaceListener eventListener) {
43 | super();
44 | this.mWidth = width;
45 | this.mHeight = height;
46 | this.eventListener = eventListener;
47 |
48 | Matrix.setLookAtM(view, 0,
49 | 0.0f, 0.0f, 1.0f,
50 | 0.0f, 0.0f, 0.0f,
51 | 0.0f, 1.0f, 0.0f
52 | );
53 | Matrix.setIdentityM(projection, 0);
54 | }
55 |
56 |
57 | @Override
58 | EglSurface createEglSurface(EglCore egl) {
59 | return new EglOffscreenSurface(egl, mWidth, mHeight);
60 | }
61 |
62 | @Override
63 | void setupDrawingResources(EglCore egl) {
64 | GlTexture glTexture = new GlTexture();
65 | fboHandler = new FBOHandler(TextureProgram.createOESProgram(glTexture));
66 | fboHandler.configure(mWidth, mHeight);
67 |
68 | surfaceTexture = new SurfaceTexture(glTexture.getId());
69 | surfaceTexture.setOnFrameAvailableListener(this);
70 | if (eventListener != null) {
71 | eventListener.onSurfaceAvailable(new Surface(surfaceTexture));
72 | }
73 | }
74 |
75 | @Override
76 | void drawFrame(int itemID) {
77 |
78 | surfaceTexture.updateTexImage();
79 | surfaceTexture.getTransformMatrix(fboHandler.getTextureTransform());
80 |
81 | makeCurrent();
82 | fboHandler.bind();
83 | fboHandler.draw(drawable, mvp);
84 |
85 | GLES20.glFinish();
86 |
87 | ByteBuffer pixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4);
88 | pixelBuf.order(ByteOrder.LITTLE_ENDIAN);
89 | GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf);
90 | Egloo.checkGlError("glReadPixels");
91 | pixelBuf.rewind();
92 |
93 | GlUtils.reverseBuf(pixelBuf, mWidth, mHeight);
94 |
95 | eventListener.onTextureRetrieved(pixelBuf);
96 |
97 | fboHandler.unbind();
98 | }
99 |
100 | public void onVideoAspectChanged(float videoAspect) {
101 | synchronized (sizeAvailableLock) {
102 | mvp = new float[16];
103 | if (videoAspect > 1) {
104 | Matrix.orthoM(projection, 0, -1 / videoAspect, 1 / videoAspect, -1, 1, -1, 1);
105 | } else {
106 | Matrix.orthoM(projection, 0, -1, 1, -videoAspect, videoAspect, -1, 1);
107 | }
108 | Matrix.multiplyMM(mvp, 0, projection, 0, view, 0);
109 |
110 | sizeAvailableLock.notifyAll();
111 | }
112 | }
113 |
114 | public void clearTxtMtx() {
115 | mvp = null;
116 | }
117 |
118 | private void waitForDimensions() {
119 | synchronized (sizeAvailableLock) {
120 | if (mvp == null) {
121 | try {
122 | Loggy.d("Waiting for clue");
123 | sizeAvailableLock.wait(TIMEOUT);
124 | if (mvp == null) {
125 | Loggy.d("Took too long");
126 | assureMvpMatrix();
127 | }
128 | } catch (InterruptedException e) {
129 | e.printStackTrace();
130 | }
131 | }
132 | }
133 | }
134 |
135 | private void assureMvpMatrix() {
136 | if (mvp == null) {
137 | mvp = new float[16];
138 | Matrix.multiplyMM(mvp, 0, projection, 0, view, 0);
139 | }
140 | }
141 |
142 | // called on drawing thread
143 | @Override
144 | public void onFrameAvailable(SurfaceTexture surfaceTexture) {
145 | Loggy.d("Frame available");
146 | waitForDimensions();
147 | requestRender(0);
148 | }
149 |
150 | public void drawSameFrame() {
151 | waitForDimensions();
152 | requestRender(55);
153 | }
154 |
155 | @Override
156 | public void release() {
157 | super.release();
158 | fboHandler.release();
159 | }
160 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/TextureProgram.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.opengl.GLES20;
4 |
5 | import com.otaliastudios.opengl.core.Egloo;
6 | import com.otaliastudios.opengl.draw.GlDrawable;
7 | import com.otaliastudios.opengl.extensions.BuffersKt;
8 | import com.otaliastudios.opengl.program.GlProgram;
9 | import com.otaliastudios.opengl.program.GlProgramLocation;
10 | import com.otaliastudios.opengl.texture.GlTexture;
11 |
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | import java.nio.FloatBuffer;
15 |
16 | public class TextureProgram extends GlProgram {
17 |
18 | public static final String SIMPLE_VERTEX_SHADER =
19 | "" +
20 | "uniform mat4 uMVPMatrix;\n" +
21 | "uniform mat4 uTexMatrix;\n" +
22 | "attribute vec4 aPosition;\n" +
23 | "attribute vec4 aTextureCoord;\n" +
24 | "varying vec2 vTextureCoord;\n" +
25 | "void main() {\n" +
26 | " gl_Position = uMVPMatrix * aPosition;\n" +
27 | " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
28 | "}\n";
29 |
30 | public static final String OES_FRAGMENT_SHADER =
31 | "" +
32 | "#extension GL_OES_EGL_image_external : require\n" +
33 | "precision mediump float;\n" +
34 | "varying vec2 vTextureCoord;\n" +
35 | "uniform samplerExternalOES sTexture;\n" +
36 | "void main() {\n" +
37 | " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
38 | "}\n";
39 |
40 | public static final String SIMPLE_FRAGMENT_SHADER =
41 | "precision mediump float;\n" +
42 | "varying highp vec2 vTextureCoord;\n" +
43 | "uniform lowp sampler2D sTexture;\n" +
44 | "void main() {\n" +
45 | "gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
46 | "}\n";
47 |
48 | private final GlProgramLocation textureTransformHandle;
49 | private final GlProgramLocation textureCoordsHandle;
50 | private final GlProgramLocation vertexPositionHandle;
51 | private final GlProgramLocation vertexMvpMatrixHandle;
52 | private final FloatBuffer textureCoordsBuffer;
53 |
54 | private float[] textureTransform;
55 | private GlTexture texture;
56 |
57 | private float[] textureCoords = {
58 | 0, 0,
59 | 1, 0,
60 | 0, 1,
61 | 1, 1
62 | };
63 |
64 | static TextureProgram createOESProgram(GlTexture texture) {
65 | return new TextureProgram(TextureProgram.SIMPLE_VERTEX_SHADER,
66 | TextureProgram.OES_FRAGMENT_SHADER, texture);
67 | }
68 |
69 | public TextureProgram(@NotNull String vertexShader, @NotNull String fragmentShader) {
70 | this(create(vertexShader, fragmentShader), true);
71 | }
72 |
73 | private TextureProgram(@NotNull String vertexShader, @NotNull String fragmentShader, GlTexture texture) {
74 | this(create(vertexShader, fragmentShader), true);
75 | this.texture = texture;
76 | }
77 |
78 | private TextureProgram(int handle, boolean ownsHandle) {
79 | super(handle, ownsHandle);
80 | textureTransform = Egloo.IDENTITY_MATRIX.clone();
81 | textureTransformHandle = getUniformHandle("uTexMatrix");
82 | textureCoordsHandle = getAttribHandle("aTextureCoord");
83 | vertexPositionHandle = getAttribHandle("aPosition");
84 | vertexMvpMatrixHandle = getUniformHandle("uMVPMatrix");
85 |
86 | textureCoordsBuffer = BuffersKt.floatBufferOf(textureCoords);
87 | }
88 |
89 | public float[] getTextureTransform() {
90 | return textureTransform;
91 | }
92 |
93 | public void setTextureTransform(float[] textureTransform) {
94 | this.textureTransform = textureTransform;
95 | }
96 |
97 | @Override
98 | public void onPreDraw(@NotNull GlDrawable drawable, @NotNull float[] modelViewProjectionMatrix) {
99 | super.onPreDraw(drawable, modelViewProjectionMatrix);
100 |
101 | if (texture != null) {
102 | texture.bind();
103 | }
104 |
105 | GLES20.glUniformMatrix4fv(vertexMvpMatrixHandle.getValue(), 1, false, modelViewProjectionMatrix, 0);
106 | Egloo.checkGlError("glUniformMatrix4fv");
107 |
108 | GLES20.glUniformMatrix4fv(textureTransformHandle.getValue(), 1, false, textureTransform, 0);
109 | Egloo.checkGlError("glUniformMatrix4fv");
110 |
111 | GLES20.glEnableVertexAttribArray(vertexPositionHandle.getValue());
112 | Egloo.checkGlError("glEnableVertexAttribArray");
113 | GLES20.glVertexAttribPointer(vertexPositionHandle.getValue(), 2,
114 | GLES20.GL_FLOAT,
115 | false,
116 | drawable.getVertexStride(),
117 | drawable.getVertexArray());
118 | Egloo.checkGlError("glVertexAttribPointer");
119 |
120 | GLES20.glEnableVertexAttribArray(textureCoordsHandle.getValue());
121 | Egloo.checkGlError("glEnableVertexAttribArray");
122 | GLES20.glVertexAttribPointer(textureCoordsHandle.getValue(), 2,
123 | GLES20.GL_FLOAT,
124 | false,
125 | drawable.getVertexStride(),
126 | textureCoordsBuffer);
127 | Egloo.checkGlError("glVertexAttribPointer");
128 | }
129 |
130 | @Override
131 | public void onPostDraw(@NotNull GlDrawable drawable) {
132 | super.onPostDraw(drawable);
133 | GLES20.glDisableVertexAttribArray(vertexPositionHandle.getValue());
134 | GLES20.glDisableVertexAttribArray(textureCoordsHandle.getValue());
135 | if (texture != null) {
136 | texture.unbind();
137 | }
138 | }
139 |
140 | @Override
141 | public void release() {
142 | super.release();
143 | if (texture != null) {
144 | texture.release();
145 | texture = null;
146 | }
147 | }
148 |
149 | public void setTexture(GlTexture texture) {
150 | this.texture = texture;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/render/TimelineGlSurfaceView.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.render;
2 |
3 | import android.content.Context;
4 | import android.opengl.GLSurfaceView;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.util.AttributeSet;
8 | import android.view.Surface;
9 |
10 | import com.google.android.exoplayer2.SimpleExoPlayer;
11 | import com.google.android.exoplayer2.video.VideoListener;
12 | import com.otaliastudios.opengl.core.EglConfigChooser;
13 | import com.otaliastudios.opengl.core.EglContextFactory;
14 | import com.video.timeline.TimelineViewFace;
15 | import com.video.timeline.ExoPlayerFactory;
16 |
17 | public class TimelineGlSurfaceView extends GLSurfaceView implements TimelineViewFace, VideoListener, GlRenderer.SurfaceEventListener {
18 |
19 | private final Handler mainHandler;
20 | private GlRenderer renderer;
21 | private SimpleExoPlayer player;
22 | private ExoPlayerFactory videoPlayerFactory;
23 | private String mediaUri;
24 |
25 | public TimelineGlSurfaceView(Context context) {
26 | this(context, null);
27 | }
28 |
29 | public TimelineGlSurfaceView(Context context, AttributeSet attrs) {
30 | super(context, attrs);
31 | setVisibility(GONE);
32 | mainHandler = new Handler(Looper.getMainLooper());
33 |
34 | setEGLContextFactory(EglContextFactory.GLES2);
35 | setEGLConfigChooser(EglConfigChooser.GLES2);
36 |
37 | setRenderer();
38 | setRenderMode(RENDERMODE_WHEN_DIRTY);
39 | }
40 |
41 | public void setRenderer() {
42 | renderer = new GlRenderer(this);
43 | setRenderer(renderer);
44 | }
45 |
46 | @Override
47 | public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
48 | float videoAspect = ((float) width / height) * pixelWidthHeightRatio;
49 | renderer.onAspectPrepared(videoAspect);
50 | requestLayout();
51 | }
52 |
53 | @Override
54 | public Context context() {
55 | return getContext();
56 | }
57 |
58 | @Override
59 | public void startSurfaceRenderer() {
60 | setVisibility(VISIBLE);
61 | renderer.setStartRendering();
62 |
63 | player = videoPlayerFactory.getPlayer(getContext());
64 | player.prepare(videoPlayerFactory.getMediaSource(mediaUri, getContext()));
65 | player.addVideoListener(this);
66 | }
67 |
68 | @Override
69 | public void releaseSurface() {
70 | renderer.release();
71 | onFinish();
72 | }
73 |
74 | @Override
75 | public void onSurfaceAvailable(Surface surface) {
76 | mainHandler.post(() -> {
77 | this.player.setVideoSurface(surface);
78 | });
79 | }
80 |
81 | @Override
82 | public void nextFrame(int offset, int limit) {
83 | requestRender();
84 | if (offset < limit) {
85 | long videoDuration = player.getDuration();
86 | long seekPos =
87 | Math.max(videoDuration / limit + offset * (videoDuration / limit), player.getCurrentPosition() + 1);
88 |
89 | player.seekTo(seekPos);
90 | }
91 | }
92 |
93 | @Override
94 | public void onFinish() {
95 | if (player != null) {
96 | player.release();
97 | player = null;
98 | }
99 | }
100 |
101 | @Override
102 | public void attachVideoFactory(ExoPlayerFactory playerFactory) {
103 | this.videoPlayerFactory = playerFactory;
104 | }
105 |
106 | @Override
107 | public void setMediaUri(String mediaUri) {
108 | this.mediaUri = mediaUri;
109 | }
110 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/tools/FileHelper.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.tools;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.os.Environment;
6 | import android.os.StatFs;
7 | import android.util.Log;
8 |
9 |
10 | import java.io.BufferedOutputStream;
11 | import java.io.File;
12 | import java.io.FileNotFoundException;
13 | import java.io.FileOutputStream;
14 | import java.io.IOException;
15 | import java.nio.ByteBuffer;
16 | import java.util.UUID;
17 |
18 | import static android.os.Build.VERSION.SDK_INT;
19 | import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
20 |
21 | public class FileHelper {
22 | private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
23 | private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
24 |
25 |
26 | private static String getName(String mediaUri, int frameIndex) {
27 | return mediaUri + "__" + frameIndex;
28 | }
29 |
30 | public static File getCachedFile(File dir, String mediaUri, int frameIndex) {
31 | return new File(dir, getName(mediaUri, frameIndex));
32 | }
33 |
34 | public static File getCachedFile(File dir, String prefix, long time) {
35 | return new File(dir, prefix + "_" + time);
36 | }
37 |
38 | public static File saveToFile(File file, ByteBuffer buffer, int width, int height) {
39 | try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath()))) {
40 | Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
41 | bmp.copyPixelsFromBuffer(buffer);
42 | bmp.compress(Bitmap.CompressFormat.JPEG, 90, bos);
43 | bmp.recycle();
44 | } catch (IOException ignored) {
45 | }
46 | return file;
47 | }
48 |
49 | public static void saveBitmapToFile(File file, Bitmap bitmap) {
50 | try {
51 | BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath()));
52 | bitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos);
53 | bos.close();
54 | } catch (FileNotFoundException e) {
55 | e.printStackTrace();
56 | } catch (IOException e) {
57 | e.printStackTrace();
58 | }
59 | }
60 |
61 | private static long calculateDiskCacheSize(File dir) {
62 | long size = MIN_DISK_CACHE_SIZE;
63 |
64 | try {
65 | StatFs statFs = new StatFs(dir.getAbsolutePath());
66 | long blockCount =
67 | SDK_INT < JELLY_BEAN_MR2 ? (long) statFs.getBlockCount() : statFs.getBlockCountLong();
68 | long blockSize =
69 | SDK_INT < JELLY_BEAN_MR2 ? (long) statFs.getBlockSize() : statFs.getBlockSizeLong();
70 | long available = blockCount * blockSize;
71 | // Target 2% of the total space.
72 | size = available / 50;
73 | } catch (IllegalArgumentException ignored) {
74 | }
75 |
76 | // Bound inside min/max size for disk cache.
77 | return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/tools/GlUtils.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.tools;
2 |
3 | import android.graphics.Bitmap;
4 | import android.os.Environment;
5 | import android.util.Log;
6 |
7 | import java.io.BufferedOutputStream;
8 | import java.io.File;
9 | import java.io.FileNotFoundException;
10 | import java.io.FileOutputStream;
11 | import java.io.IOException;
12 | import java.nio.ByteBuffer;
13 | import java.util.UUID;
14 |
15 | public class GlUtils {
16 |
17 | public static String savePixelBuffer(ByteBuffer pixelBuffer, int width, int height) throws IOException {
18 | File parent = new File(Environment.getExternalStorageDirectory() + "/AScreen");
19 | if (parent.exists() || parent.mkdirs()) {
20 | reverseBuf(pixelBuffer, width, height);
21 | File file = new File(parent, UUID.randomUUID() + ".jpg");
22 | Log.d("pbo_test", "Saving: " + file.getName());
23 | BufferedOutputStream bos = null;
24 | try {
25 | bos = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath()));
26 | Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
27 | bmp.copyPixelsFromBuffer(pixelBuffer);
28 | bmp.compress(Bitmap.CompressFormat.JPEG, 90, bos);
29 | bmp.recycle();
30 | } finally {
31 | if (bos != null) bos.close();
32 | }
33 |
34 | return file.getAbsolutePath();
35 | }
36 |
37 | return null;
38 | }
39 |
40 | public static void reverseBuf(ByteBuffer buf, int width, int height) {
41 | long ts = System.currentTimeMillis();
42 | int i = 0;
43 | byte[] tmp = new byte[width * 4];
44 | while (i++ < height / 2) {
45 | buf.get(tmp);
46 | System.arraycopy(buf.array(), buf.limit() - buf.position(), buf.array(), buf.position() - width * 4, width * 4);
47 | System.arraycopy(tmp, 0, buf.array(), buf.limit() - buf.position(), width * 4);
48 | }
49 | buf.rewind();
50 | Log.d("pbo_test", "reverseBuf took " + (System.currentTimeMillis() - ts) + "ms");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/tools/Loggy.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.tools;
2 |
3 | import android.util.Log;
4 |
5 | public class Loggy {
6 | private static final String TAG = "TimeLineView";
7 |
8 | public static void d(String msg) {
9 | Log.d(TAG, msg);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/library/src/main/java/com/video/timeline/tools/MediaHelper.java:
--------------------------------------------------------------------------------
1 | package com.video.timeline.tools;
2 |
3 | import android.content.Context;
4 | import android.database.Cursor;
5 | import android.media.MediaMetadataRetriever;
6 | import android.net.Uri;
7 | import android.provider.MediaStore;
8 | import android.text.TextUtils;
9 |
10 | import com.video.timeline.VideoMetadata;
11 | import com.video.timeline.tools.Loggy;
12 |
13 | public class MediaHelper {
14 |
15 | public static void getVideoMets(Context context, String media, VideoMetadata metadata) {
16 | MediaMetadataRetriever retriever = new MediaMetadataRetriever();
17 | retriever.setDataSource(context, Uri.parse(media));
18 | String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
19 | String widthString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
20 | String heightString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
21 |
22 | try {
23 | metadata.setDurationMs(!TextUtils.isEmpty(time) ? Long.parseLong(time) : getDuration(context, media));
24 | metadata.setWidth(!TextUtils.isEmpty(widthString) ? Integer.parseInt(widthString) : 0);
25 | metadata.setHeight(!TextUtils.isEmpty(heightString) ? Integer.parseInt(heightString) : 0);
26 | retriever.release();
27 |
28 | } catch (NumberFormatException ex) {
29 | Loggy.d("Error retrieving: " + ex);
30 | }
31 | }
32 |
33 | private static long getDuration(Context context, String media) {
34 | if (media.startsWith("content://")) {
35 | return getContentDuration(context, media);
36 | } else {
37 | return getFileDuration(context, media);
38 | }
39 | }
40 |
41 |
42 | private static long getContentDuration(Context context, String media) {
43 | Uri uri = Uri.parse(media);
44 |
45 | long duration = 0;
46 | Cursor cursor = MediaStore.Video.query(context.getContentResolver(),
47 | uri, new String[] {"duration"});
48 | if (cursor.moveToFirst()) {
49 | duration = cursor.getLong(cursor.getColumnIndex("duration"));
50 | }
51 | cursor.close();
52 |
53 | return duration;
54 | }
55 |
56 | private static long getFileDuration(Context context, String filePath) {
57 | Cursor cursor = context.getContentResolver().query(
58 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
59 | new String[] {"duration"},
60 | MediaStore.Video.Media.DATA + "=?",
61 | new String[] { filePath } ,
62 | null);
63 |
64 | if (cursor != null && cursor.moveToFirst()) {
65 | long duration = cursor.getLong(cursor.getColumnIndex("duration"));
66 | cursor.close();
67 | return duration;
68 | }
69 | return 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/screens/1_shot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chemicalbird/ExoPlayerTimelineView/821d98c11c98d730044c993713ad8a5d062fa56c/screens/1_shot.jpg
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name='My Application'
2 | include ':app'
3 | include ':library'
4 |
--------------------------------------------------------------------------------