├── .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 |