前言
Android保存网络视频到本地已经是很常见的事情了,刚好今天又遇到这个需求就记录一下,希望能帮到正在找的码友!
一、导入依赖
示例:一般项目都会有这些依赖的
dependencies {
// OkHttp 网络请求库
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// 核心库
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
}
二、工具类
package com.hbxyy.teacherassistant.util
import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.OutputStream
import java.util.concurrent.TimeUnit
/**
* Created by caoliulang.
* Date: 2026/2/26
* Description :
*/
object VideoDownloadUtil {
// 初始化OkHttp客户端
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
/**
* 下载网络视频并保存到相册
* @param context 上下文
* @param videoUrl 视频网络地址
* @param videoName 保存到相册的视频名称(不带后缀)
* @param onComplete 完成回调(success: 是否成功, msg: 提示信息)
*/
suspend fun downloadAndSaveToGallery(
context: Context,
videoUrl: String,
videoName: String = "video_${System.currentTimeMillis()}",
onComplete: (Boolean, String) -> Unit
) = withContext(Dispatchers.IO) {
try {
// 1. 检查Android 10以下的存储权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val hasPermission = context.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
withContext(Dispatchers.Main) {
onComplete(false, "需要存储权限才能保存视频")
}
return@withContext
}
}
// 2. 下载视频到临时目录
val tempFile = downloadVideoToTempDir(context, videoUrl) ?: run {
withContext(Dispatchers.Main) {
onComplete(false, "视频下载失败")
}
return@withContext
}
// 3. 保存临时文件到相册
val saveSuccess = saveTempVideoToGallery(context, tempFile, videoName)
// 4. 删除临时文件(释放空间)
if (tempFile.exists()) {
tempFile.delete()
}
// 5. 回调结果
withContext(Dispatchers.Main) {
val msg = if (saveSuccess) "视频保存到相册成功" else "视频保存到相册失败"
onComplete(saveSuccess, msg)
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
onComplete(false, "处理失败:${e.message ?: "未知错误"}")
}
}
}
/**
* 下载视频到APP私有临时目录
*/
private fun downloadVideoToTempDir(context: Context, videoUrl: String): File? {
return try {
// 创建临时文件(APP私有目录,无需权限)
val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.mp4")
val request = Request.Builder().url(videoUrl).build()
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful || response.body() == null) {
return null
}
// 将响应体写入临时文件
response.body()!!.byteStream().use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
// 检查文件是否有效
if (tempFile.length() <= 0) {
tempFile.delete()
return null
}
tempFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将临时视频文件保存到相册
*/
private fun saveTempVideoToGallery(context: Context, tempFile: File, videoName: String): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ 使用MediaStore
saveToGalleryQ(context, tempFile, videoName)
} else {
// Android 10以下 传统方式
saveToGalleryLegacy(context, tempFile, videoName)
}
}
/**
* Android 10+ 保存逻辑
*/
private fun saveToGalleryQ(context: Context, tempFile: File, videoName: String): Boolean {
val contentValues = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, "$videoName.mp4")
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES)
put(MediaStore.Video.Media.SIZE, tempFile.length())
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return false
return try {
// 复制文件内容到相册
resolver.openOutputStream(uri)?.use { outputStream ->
FileInputStream(tempFile).use { inputStream ->
inputStream.copyTo(outputStream)
}
}
true
} catch (e: Exception) {
resolver.delete(uri, null, null) // 失败时删除空记录
false
}
}
/**
* Android 10以下 保存逻辑
*/
private fun saveToGalleryLegacy(context: Context, tempFile: File, videoName: String): Boolean {
// 相册视频目录
val galleryDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
if (!galleryDir.exists()) galleryDir.mkdirs()
val targetFile = File(galleryDir, "$videoName.mp4")
if (targetFile.exists()) targetFile.delete()
return try {
// 复制文件
FileInputStream(tempFile).use { input ->
FileOutputStream(targetFile).use { output ->
input.copyTo(output)
}
}
// 通知媒体库扫描,确保相册显示
MediaScannerConnection.scanFile(
context,
arrayOf(targetFile.absolutePath),
arrayOf("video/mp4")
) { _, _ -> }
true
} catch (e: Exception) {
false
}
}
}
三、调用
private val REQUEST_STORAGE_PERMISSION = 1001
//调用
if (intent.getStringExtra("url").toString().isEmpty()) {
ToastUtils.showToast("视频地址有问题不支持下载")
return
}
// Android 10以下先申请权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_STORAGE_PERMISSION
)
return
}
}
// 执行下载和保存
downloadVideo(intent.getStringExtra("url").toString())
/**
* 下载并保存视频
*/
private fun downloadVideo(videoUrl: String) {
CoroutineScope(Dispatchers.Main).launch {
DialogUtils1.showLoadingDialog(this@WorkLibraryDetails)
VideoDownloadUtil.downloadAndSaveToGallery(
context = this@WorkLibraryDetails,
videoUrl = videoUrl,
videoName = "my_download_video"
) { success, msg ->
ToastUtils.showToast("视频已下载到相册!")
DialogUtils1.hideLoadingDialog()
}
}
}
/**
* 权限申请结果回调
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_STORAGE_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限申请成功,执行下载
if (intent.getStringExtra("url").toString().isNotEmpty()) {
downloadVideo(intent.getStringExtra("url").toString())
}
} else {
ToastUtils.showToast("拒绝存储权限将无法保存视频")
}
}
}
四、权限配置
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Android 10以下存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Android 13+ 读取媒体权限(可选) -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"
android:minSdkVersion="33" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
总结
方法就是那么简单,已经避开很多坑了,放心使用,如有问题欢迎指导!
6409

被折叠的 条评论
为什么被折叠?



