fix: Android 4.2 兼容性修复 - 降级第三方库 + 移除 ExoPlayer + multidex 配置
1. OkHttp 3.14.9 -> 3.12.0(Android10Platform 引用 Android 10+ API 导致 VerifyError) 2. Gson 2.10.1 -> 2.8.6(ReflectionHelper 引用 Java 9+ API 导致 VerifyError) 3. Retrofit 2.9.0 -> 2.6.0(与客户环境一致,配合 OkHttp 3.12.0) 4. Coroutines 1.7.3 -> 1.6.4(1.7+ 要求 minSdk 21) 5. 移除 ExoPlayer 2.19.1,改用系统原生 MediaPlayer + SurfaceView 6. 主题 MaterialComponents -> AppCompat(避免 MaterialComponentsViewInflater multidex 问题) 7. multidex-config.pro 新增 kotlinx.coroutines.** 和 retrofit2.** 到主 dex 8. 新增 Android 4.2 兼容性改动文档
This commit is contained in:
156
Android4.2兼容性改动文档.md
Normal file
156
Android4.2兼容性改动文档.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Android 4.2 兼容性改动文档
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
项目 original 目标为 Android 5.0+,需降级兼容 Android 4.2(API 17)。Android 4.2 使用 Dalvik 虚拟机(非 ART),对 multidex 和高版本 API 引用有严格限制,导致一系列兼容性问题。
|
||||||
|
|
||||||
|
## 问题分类
|
||||||
|
|
||||||
|
兼容性问题分为两大类:
|
||||||
|
|
||||||
|
1. **Dalvik multidex 类加载失败**:Dalvik 在框架级反射和内部类加载场景下,无法从 secondary dex 加载类,导致 `ClassNotFoundException` / `NoClassDefFoundError` / `VerifyError`
|
||||||
|
2. **高版本 API 引用**:第三方库的某些类引用了 Android 10+ 或 Java 9+ 的 API,Dalvik 验证器拒绝加载,导致 `VerifyError`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、Multidex 配置改动
|
||||||
|
|
||||||
|
### 1.1 multidex-config.pro(主 dex 保留类)
|
||||||
|
|
||||||
|
**文件**:`app/multidex-config.pro`
|
||||||
|
|
||||||
|
**原因**:Dalvik multidex 环境下,以下场景无法从 secondary dex 加载类:
|
||||||
|
- `LayoutInflater.createViewFromTag`(框架级反射创建 View)
|
||||||
|
- `AppCompatDelegateImpl.createSubDecor`(inflate 内部装饰布局)
|
||||||
|
- 协程内部机制加载 `CoroutineExceptionHandlerImplKt`
|
||||||
|
- Retrofit `KotlinExtensions.await()` 被 `HttpServiceMethod` 运行时调用
|
||||||
|
|
||||||
|
**保留到主 dex 的包**:
|
||||||
|
|
||||||
|
```
|
||||||
|
- com.pukka.mediaapp.** # 项目自身所有类
|
||||||
|
- androidx.appcompat.app.** # AppCompatDelegateImpl、AppCompatViewInflater
|
||||||
|
- androidx.appcompat.widget.** # FitWindowsLinearLayout、AppCompatButton 等
|
||||||
|
- kotlinx.coroutines.** # 协程核心类
|
||||||
|
- retrofit2.** # KotlinExtensions、HttpServiceMethod 等
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 AndroidManifest.xml
|
||||||
|
|
||||||
|
**文件**:`app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
**改动**:
|
||||||
|
- 移除 `com.google.android.exoplayer2` 的 `overrideLibrary`(已移除 ExoPlayer 依赖)
|
||||||
|
- 保留其他 AndroidX 库的 `overrideLibrary`(core-ktx 1.12.0、appcompat 1.6.1 等的 minSdk 为 19-21,通过 override 覆盖为 17)
|
||||||
|
- 移除 `androidx.startup.InitializationProvider`(Android 4.x Dalvik multidex 下 Provider 启动时无法加载 secondary dex 中的初始化类,App 手动初始化所有组件)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、第三方库版本降级
|
||||||
|
|
||||||
|
### 2.1 OkHttp:3.14.9 → 3.12.0
|
||||||
|
|
||||||
|
**问题**:OkHttp 3.12.1+ 包含 `Android10Platform` 类,引用 Android 10+ 的 SSL API(`SSLParameters.setApplicationProtocols`)。`Platform.findAndroidPlatform()` 在静态初始化时尝试加载该类,Dalvik 验证器拒绝 → `VerifyError`
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
```groovy
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
|
||||||
|
// 防止其他依赖传递引入 3.12.1+
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Gson:2.10.1 → 2.8.6
|
||||||
|
|
||||||
|
**问题**:Gson 2.9+ 引入 `ReflectionHelper` 类,引用 Java 9+ 的反射 API(`java.lang.invoke.MethodHandles`)。`ReflectiveTypeAdapterFactory.create()` 调用该类,Dalvik 验证器拒绝 → `VerifyError`
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
```groovy
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Retrofit:2.9.0 → 2.6.0
|
||||||
|
|
||||||
|
**问题**:Retrofit 2.9.0 传递依赖 OkHttp 3.14.9(含 `Android10Platform`)。与客户环境保持一致使用 2.6.0。
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
```groovy
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
|
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Kotlin Coroutines:1.7.3 → 1.6.4
|
||||||
|
|
||||||
|
**问题**:kotlinx-coroutines 1.7+ 要求 minSdk 21(Android 5.0),1.6.4 兼容 API 16+。
|
||||||
|
|
||||||
|
**修复**:
|
||||||
|
```groovy
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、ExoPlayer 移除,改用系统原生 MediaPlayer
|
||||||
|
|
||||||
|
**问题**:ExoPlayer 2.14+ 要求 minSdk 21,2.19.1 的类大量引用 Android 5+ API,在 Android 4.2 上会 `VerifyError`。即使降到 2.13.0,API 差异大且仍有兼容性风险。
|
||||||
|
|
||||||
|
**修复**:完全移除 ExoPlayer,改用 Android 系统原生 `MediaPlayer` + `SurfaceView`(API 1+,零兼容性问题)。
|
||||||
|
|
||||||
|
**涉及文件**:
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `app/build.gradle` | 移除 `exoplayer:2.19.1` 依赖 |
|
||||||
|
| `app/src/main/AndroidManifest.xml` | 移除 `com.google.android.exoplayer2` overrideLibrary |
|
||||||
|
| `app/src/main/res/layout/activity_player.xml` | `PlayerView` → `SurfaceView` |
|
||||||
|
| `app/src/main/java/.../ui/VideoPlayerActivity.kt` | 重写为 `MediaPlayer` + `SurfaceHolder.Callback` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、主题改动
|
||||||
|
|
||||||
|
**文件**:`app/src/main/res/values/themes.xml`
|
||||||
|
|
||||||
|
**改动**:`Theme.MaterialComponents.NoActionBar` → `Theme.AppCompat.NoActionBar`
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- `MaterialComponentsViewInflater` 继承 `AppCompatViewInflater`,被框架反射加载时需在主 dex
|
||||||
|
- 保留整个 `com.google.android.material.**` 到主 dex 会导致方法数超限(80949 > 65536)
|
||||||
|
- 布局中只用了标准控件(TextView/EditText/Button/ProgressBar),无需 Material 主题
|
||||||
|
- 改用 AppCompat 主题后,使用 `AppCompatViewInflater`,避免 Material 的 multidex 问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、build.gradle 完整依赖对照
|
||||||
|
|
||||||
|
| 依赖 | 修复前 | 修复后 | 原因 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| retrofit | 2.9.0 | **2.6.0** | 传递依赖 OkHttp 3.14.9 不兼容 |
|
||||||
|
| converter-gson | 2.9.0 | **2.6.0** | 同上 |
|
||||||
|
| okhttp | 3.14.9(传递) | **3.12.0**(force) | Android10Platform VerifyError |
|
||||||
|
| logging-interceptor | 3.12.12 | **3.12.0** | 同上 |
|
||||||
|
| gson | 2.10.1 | **2.8.6** | ReflectionHelper VerifyError |
|
||||||
|
| coroutines | 1.7.3 | **1.6.4** | 1.7+ 要求 minSdk 21 |
|
||||||
|
| exoplayer | 2.19.1 | **移除** | minSdk 21,改用 MediaPlayer |
|
||||||
|
| appcompat | 1.6.1 | 1.6.1(不变) | overrideLibrary 覆盖 |
|
||||||
|
| core-ktx | 1.12.0 | 1.12.0(不变) | overrideLibrary 覆盖 |
|
||||||
|
| material | 1.11.0 | 1.11.0(不变) | 改用 AppCompat 主题 |
|
||||||
|
| lifecycle | 2.3.1 | 2.3.1(不变) | 兼容 API 16+ |
|
||||||
|
| activity | 1.3.1 | 1.3.1(不变) | 兼容 API 16+ |
|
||||||
|
| multidex | 2.0.1 | 2.0.1(不变) | 兼容 API 16+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、问题排查记录(按发现顺序)
|
||||||
|
|
||||||
|
| 序号 | 错误 | 根因 | 修复 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1 | `VerifyError: AppCompatViewInflater` | Dalvik multidex 无法加载 secondary dex 的 AppCompat 类 | multidex-config.pro 保留 `androidx.appcompat.**` |
|
||||||
|
| 2 | `ClassNotFoundException: FitWindowsLinearLayout` | AppCompat 内部 widget 不以 `AppCompat` 开头,通配符未覆盖 | 改用包级别 `androidx.appcompat.widget.**` |
|
||||||
|
| 3 | `NoClassDefFoundError: CoroutineExceptionHandlerImplKt` | 协程内部类加载失败 | multidex-config.pro 保留 `kotlinx.coroutines.**` |
|
||||||
|
| 4 | `VerifyError: Android10Platform` | OkHttp 3.12.1+ 引用 Android 10+ API | OkHttp 降级 3.12.0 + force 锁定 |
|
||||||
|
| 5 | `VerifyError: ReflectionHelper` | Gson 2.9+ 引用 Java 9+ API | Gson 降级 2.8.6 |
|
||||||
|
| 6 | `NoClassDefFoundError: retrofit2.KotlinExtensions` | Retrofit suspend 支持类在 secondary dex | multidex-config.pro 保留 `retrofit2.**` |
|
||||||
|
| 7 | 主 dex 方法数超限 80949 > 65536 | 保留 material.** 到主 dex | 改用 AppCompat 主题,移除 material 保留 |
|
||||||
@@ -72,23 +72,25 @@ dependencies {
|
|||||||
// multidex
|
// multidex
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
|
||||||
// Retrofit + OkHttp(3.12.x 兼容 API 16+)
|
// Retrofit 2.6.0 + OkHttp 3.12.0(与客户环境一致,兼容 Android 4.x)
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
// Retrofit 2.6.0 自然依赖 OkHttp 3.12.0(无 Android10Platform,不会 VerifyError)
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
// OkHttp 3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.12'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
|
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
|
||||||
|
|
||||||
// Gson
|
// Gson 2.8.6(2.9+ 引入 ReflectionHelper,引用 Java 9+ API,在 Android 4.x 上 VerifyError)
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines 1.6.4(1.7+ 要求 minSdk 21,1.6.4 兼容 API 16+)
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
|
|
||||||
// JetsenxSdk + NewTVSDK
|
// JetsenxSdk + NewTVSDK
|
||||||
implementation files('libs/JetsenxSdk-api-future-release-v1.23.aar')
|
implementation files('libs/JetsenxSdk-api-future-release-v1.23.aar')
|
||||||
implementation files('libs/NewTVSDK-v4.0.3-app-lite.jar')
|
implementation files('libs/NewTVSDK-v4.0.3-app-lite.jar')
|
||||||
|
|
||||||
// ExoPlayer
|
// 视频播放使用系统原生 MediaPlayer + SurfaceView,无需 ExoPlayer
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.19.1'
|
|
||||||
|
|
||||||
// commons-codec(SDK 依赖)
|
// commons-codec(SDK 依赖)
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
@@ -103,3 +105,9 @@ dependencies {
|
|||||||
implementation 'net.jpountz.lz4:lz4:1.3.0'
|
implementation 'net.jpountz.lz4:lz4:1.3.0'
|
||||||
implementation 'org.slf4j:slf4j-android:1.7.36'
|
implementation 'org.slf4j:slf4j-android:1.7.36'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 强制 OkHttp 3.12.0(3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError)
|
||||||
|
// 其他依赖传递引入 3.12.13,需 force 锁定 3.12.0
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,28 @@
|
|||||||
# Android 4.x Dalvik multidex 兼容配置
|
# Android 4.x Dalvik multidex 兼容配置
|
||||||
# InitializationProvider 已从 manifest 移除,仅需保留 App 自身启动类
|
# InitializationProvider 已从 manifest 移除,仅需保留 App 自身启动类
|
||||||
|
#
|
||||||
|
# 核心原则:Dalvik multidex 下,只有以下两种场景无法加载 secondary dex 中的类
|
||||||
|
# 1. Android 框架级反射:LayoutInflater.createView 使用框架 ClassLoader
|
||||||
|
# 创建 View 时找不到 secondary dex 中的 AppCompat 控件类
|
||||||
|
# 2. 协程内部类加载:kotlinx.coroutines 内部加载 CoroutineExceptionHandlerImplKt
|
||||||
|
# 等核心类时,在 Dalvik multidex 下失败
|
||||||
|
# 注意:已将主题从 MaterialComponents 改为 AppCompat,避免 MaterialComponentsViewInflater
|
||||||
|
# 的 multidex 类加载问题,同时减少主 dex 方法数(material 包约 20000+ 方法)
|
||||||
|
|
||||||
# App 自身核心类
|
# === 项目自身所有类 ===
|
||||||
-keep class com.pukka.mediaapp.PartnerApp { *; }
|
-keep class com.pukka.mediaapp.** { *; }
|
||||||
-keep class com.pukka.mediaapp.ui.PartnerLaunchActivity { *; }
|
|
||||||
|
# === AppCompat 完整包(app + widget)===
|
||||||
|
# LayoutInflater.createViewFromTag 反射创建:FitWindowsLinearLayout、AppCompatButton 等
|
||||||
|
# AppCompatDelegateImpl.createSubDecor inflate 内部装饰布局
|
||||||
|
-keep class androidx.appcompat.app.** { *; }
|
||||||
|
-keep class androidx.appcompat.widget.** { *; }
|
||||||
|
|
||||||
|
# === Kotlin Coroutines ===
|
||||||
|
# 协程启动时内部加载 CoroutineExceptionHandlerImplKt(无法从 secondary dex 加载)
|
||||||
|
# 覆盖 flow(StateFlow)、android(Dispatchers)、internal 等
|
||||||
|
-keep class kotlinx.coroutines.** { *; }
|
||||||
|
|
||||||
|
# === Retrofit ===
|
||||||
|
# KotlinExtensions.await() 被 HttpServiceMethod 在运行时调用,suspend 函数支持
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
<!-- 覆盖依赖库的 minSdk 要求,兼容 Android 4.2(API 17) -->
|
<!-- 覆盖依赖库的 minSdk 要求,兼容 Android 4.2(API 17) -->
|
||||||
<uses-sdk tools:overrideLibrary="
|
<uses-sdk tools:overrideLibrary="
|
||||||
com.google.android.exoplayer2,
|
|
||||||
androidx.core,
|
androidx.core,
|
||||||
androidx.appcompat,
|
androidx.appcompat,
|
||||||
com.google.android.material,
|
com.google.android.material,
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
package com.pukka.mediaapp.ui
|
package com.pukka.mediaapp.ui
|
||||||
|
|
||||||
|
import android.media.MediaPlayer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
import android.view.SurfaceView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
|
||||||
import com.google.android.exoplayer2.MediaItem
|
|
||||||
import com.google.android.exoplayer2.Player
|
|
||||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
|
||||||
import com.pukka.mediaapp.databinding.ActivityPlayerBinding
|
import com.pukka.mediaapp.databinding.ActivityPlayerBinding
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视频播放页面 - 使用 ExoPlayer
|
* 视频播放页面 - 使用系统原生 MediaPlayer + SurfaceView
|
||||||
|
* 兼容 Android 4.2+,无需第三方播放器库
|
||||||
*/
|
*/
|
||||||
class VideoPlayerActivity : AppCompatActivity() {
|
class VideoPlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityPlayerBinding
|
private lateinit var binding: ActivityPlayerBinding
|
||||||
private var player: ExoPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var hasError = false
|
private var hasError = false
|
||||||
|
private var videoUrl: String = ""
|
||||||
|
private var surfaceReady = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -38,7 +37,7 @@ class VideoPlayerActivity : AppCompatActivity() {
|
|||||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL).orEmpty()
|
videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL).orEmpty()
|
||||||
val title = intent.getStringExtra(EXTRA_TITLE).orEmpty()
|
val title = intent.getStringExtra(EXTRA_TITLE).orEmpty()
|
||||||
|
|
||||||
Log.d(TAG, "onCreate: videoUrl=$videoUrl title=$title")
|
Log.d(TAG, "onCreate: videoUrl=$videoUrl title=$title")
|
||||||
@@ -50,55 +49,71 @@ class VideoPlayerActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPlayer(videoUrl)
|
setupPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPlayer(videoUrl: String) {
|
private fun setupPlayer() {
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
val userAgent = Util.getUserAgent(this, "PartnerTestApp")
|
|
||||||
|
|
||||||
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
val surfaceView = binding.surfaceView
|
||||||
.setUserAgent(userAgent)
|
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
|
||||||
.setConnectTimeoutMs(15000)
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
.setReadTimeoutMs(30000)
|
Log.d(TAG, "surfaceCreated")
|
||||||
.setAllowCrossProtocolRedirects(true)
|
surfaceReady = true
|
||||||
|
startPlayback()
|
||||||
val dataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory)
|
|
||||||
|
|
||||||
player = ExoPlayer.Builder(this)
|
|
||||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
binding.playerView.player = player
|
|
||||||
binding.playerView.useController = true
|
|
||||||
binding.playerView.controllerAutoShow = true
|
|
||||||
binding.playerView.controllerHideOnTouch = true
|
|
||||||
binding.playerView.controllerShowTimeoutMs = 5000
|
|
||||||
|
|
||||||
val mediaItem = MediaItem.fromUri(Uri.parse(videoUrl))
|
|
||||||
player?.setMediaItem(mediaItem)
|
|
||||||
player?.prepare()
|
|
||||||
player?.playWhenReady = true
|
|
||||||
player?.volume = 1f
|
|
||||||
|
|
||||||
player?.addListener(object : Player.Listener {
|
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
|
||||||
when (state) {
|
|
||||||
Player.STATE_READY -> showLoading(false)
|
|
||||||
Player.STATE_BUFFERING -> showLoading(true)
|
|
||||||
Player.STATE_ENDED -> showLoading(false)
|
|
||||||
Player.STATE_IDLE -> showLoading(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: com.google.android.exoplayer2.PlaybackException) {
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
|
||||||
Log.e(TAG, "onPlayerError", error)
|
|
||||||
showLoading(false)
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
showError("播放错误: ${error.message}")
|
Log.d(TAG, "surfaceDestroyed")
|
||||||
|
surfaceReady = false
|
||||||
|
mediaPlayer?.release()
|
||||||
|
mediaPlayer = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startPlayback() {
|
||||||
|
if (!surfaceReady) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaPlayer = MediaPlayer().apply {
|
||||||
|
setDataSource(this@VideoPlayerActivity, Uri.parse(videoUrl))
|
||||||
|
setDisplay(binding.surfaceView.holder)
|
||||||
|
|
||||||
|
setOnPreparedListener { mp ->
|
||||||
|
Log.d(TAG, "onPrepared, duration=${mp.duration}")
|
||||||
|
showLoading(false)
|
||||||
|
mp.start()
|
||||||
|
mp.setVolume(1f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCompletionListener {
|
||||||
|
Log.d(TAG, "onCompletion")
|
||||||
|
showLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnErrorListener { mp, what, extra ->
|
||||||
|
Log.e(TAG, "onError what=$what extra=$extra")
|
||||||
|
showLoading(false)
|
||||||
|
showError("播放错误: $what/$extra")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnBufferingUpdateListener { _, percent ->
|
||||||
|
Log.d(TAG, "onBufferingUpdate percent=$percent")
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareAsync()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "startPlayback failed", e)
|
||||||
|
showLoading(false)
|
||||||
|
showError("播放失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showLoading(show: Boolean) {
|
private fun showLoading(show: Boolean) {
|
||||||
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
@@ -108,28 +123,30 @@ class VideoPlayerActivity : AppCompatActivity() {
|
|||||||
showLoading(false)
|
showLoading(false)
|
||||||
binding.tvError.text = message
|
binding.tvError.text = message
|
||||||
binding.tvError.visibility = View.VISIBLE
|
binding.tvError.visibility = View.VISIBLE
|
||||||
binding.playerView.visibility = View.GONE
|
binding.surfaceView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (!hasError) player?.playWhenReady = true
|
if (surfaceReady && mediaPlayer != null && !hasError) {
|
||||||
|
mediaPlayer?.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
player?.playWhenReady = false
|
mediaPlayer?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
player?.stop()
|
mediaPlayer?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
player?.release()
|
mediaPlayer?.release()
|
||||||
player = null
|
mediaPlayer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#000000">
|
android:background="#000000">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerView
|
<SurfaceView
|
||||||
android:id="@+id/playerView"
|
android:id="@+id/surfaceView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent" />
|
||||||
app:resize_mode="fit" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<style name="Theme.PartnerTest" parent="Theme.MaterialComponents.NoActionBar">
|
<style name="Theme.PartnerTest" parent="Theme.AppCompat.NoActionBar">
|
||||||
<item name="colorPrimary">#444466</item>
|
<item name="colorPrimary">#444466</item>
|
||||||
<item name="colorPrimaryDark">#1A1A2E</item>
|
<item name="colorPrimaryDark">#1A1A2E</item>
|
||||||
<item name="colorAccent">#00FF88</item>
|
<item name="colorAccent">#00FF88</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user