视频的存储与使用

视频的存储与使用

数据库可以存储视频,但是通常不会将大型文件(如视频、音频等)直接存储在数据库中,因为这会导致数据库变得过于庞大,降低数据库的性能,增加维护成本。

通常更好的做法是将视频文件存储在服务器的文件系统中,然后在数据库中保存该视频文件的链接地址(比如下面用的服务器上的URL),通过链接地址获取视频文件。

转为base64编码方法

视频文件可以生成base64编码。base64编码是一种将二进制数据转换为ASCII字符串的编码方式,因此任何类型的文件,包括视频文件,都可以通过base64编码进行转换和传输。

1
2
3
4
5
6
// 读取视频文件
$video_path = 'path/to/video.mp4';
$video_data = file_get_contents($video_path);

// 将视频数据转换为base64编码
$video_base64 = base64_encode($video_data);
1
2
3
4
5
6
7
8
9
10
11
12
13
val video_base64 = jsonResponse.getString("video_base64")

// 解码Base64字符串为字节数组
val videoData = Base64.decode(video_base64, Base64.DEFAULT)

// 将字节数组写入临时文件
val videoFile = File.createTempFile("temp_video", ".mp4")
videoFile.writeBytes(videoData)

// 将临时文件路径传递给VideoView进行播放
val videoView: VideoView = findViewById(R.id.video_view)
videoView.setVideoPath(videoFile.absolutePath)
videoView.start()

弊端:生成base64编码会将数据大小增加约1.33倍,对于大型视频文件,会导致性能和带宽占用问题。

Allowed memory size of 134217728 bytes exhausted (tried to allocate 68964352 bytes)

对于内存稍微大一点的视频文件,会出现这个问题。

原因:PHP脚本尝试使用的内存超过了PHP配置文件中允许使用的最大内存限制。默认情况下,PHP配置文件中的memory_limit指令设置为128M,因此当使用的内存超过此限制时,就会出现错误。

解决:

  1. 增加内存限制:开头使用ini_set('memory_limit', '256M');增加内存限制。
  2. 优化代码:优化代码来减少内存使用量。例如,在循环中使用unset()函数释放不需要的变量和数组;使用mysqli_fetch_array()替代mysqli_fetch_all()等方法。
  3. 分割任务:如果你需要处理大量数据,可以分割为多个步骤,在每个步骤中处理一部分数据,避免一次性处理大量数据导致内存不足的问题。

注:使用ini_set()函数修改内存限制之前,需要检查是否有足够的可用内存。如果服务器上没有足够的可用内存,那么增加内存限制可能会导致服务器崩溃。最好的方案是优化代码,减少内存使用量。

我的解决方法:

使用分块传输的方法,将Base64字符串分成多个较小的块进行传输,避免一次传输大量数据导致网络传输过慢或者内存不足的问题

步骤:
  1. 在服务器端,将视频文件读入内存,转换为Base64字符串。
  2. 将Base64字符串按照一定的块大小进行分块,每个块不超过一定的大小(一般为1MB),将每个块的数据单独发送给客户端。
  3. 客户端收到数据后,将每个块的数据拼接在一起,得到完整的Base64字符串。
  4. 客户端使用Base64字符串解码并播放视频。

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini_set('memory_limit', '256M');

// 读取视频文件
$file = '/path/to/video.mp4';
$data = file_get_contents($file);

// 转换为Base64字符串
$base64 = base64_encode($data);

// 分块传输数据
$block_size = 1048576; // 1MB
$chunks = str_split($base64, $block_size);
foreach ($chunks as $chunk) {
// 发送数据块
echo $chunk;
// 刷新输出缓冲区,确保数据即时发送
flush();
ob_flush();
}

// 下面是拼接块,不需要使用,仅记录
// 拼接分块数据为一个完整数据
$completeData = implode('', $chunks);

// 返回拼接后的完整数据
return $completeData;

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 建立HTTP连接
val url = URL("http://example.com/video.php")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"

// 接收数据块并拼接
val base64 = StringBuilder()
val input: InputStream = connection.inputStream
val buffer = ByteArray(1024)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } > 0) {
base64.append(String(buffer, 0, bytesRead))
}

// 解码并播放视频
val base64String = base64.toString()
val data = Base64.decode(base64String, Base64.DEFAULT)
val file = File(getExternalFilesDir(null), "video.mp4")
val output = FileOutputStream(file)
output.write(data)
output.close()

注:使用1MB大小的块可以减少网络请求的次数,从而提高文件下载速度。同时,1MB也是一种比较合理的块大小,可以避免在内存中占用太多空间,同时又不会增加太多的I/O操作。

客户端直接从URL下载视频文件

DownloadManager 类来执行文件下载

1
2
3
4
5
6
7
8
9
val downloadUrl = "视频路径  .mp4"
val fileName = "myvideo.mp4"
val request = DownloadManager.Request(Uri.parse(downloadUrl))
.setTitle(fileName)
.setDescription("Downloading")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.enqueue(request)

downloadUrl 是视频文件的 URL

fileName 是下载后保存的文件名

DownloadManager.Request 设置文件的一些属性,如下载时显示的名称、描述和存储位置等

DownloadManager 在执行下载时,将下载请求加入到下载队列中

下载文件存储在外部公共目录的 DOWNLOADS 目录下,用户要在设备上使用其他应用程序访问这些文件,需要在清单文件中添加权限:

1
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
使用视频文件
1
2
3
4
val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/myvideo.mp4"
val videoView = findViewById<VideoView>(R.id.videoView)
videoView.setVideoPath(filePath)
videoView.setOnPreparedListener { mp -> mp.start() }

setVideoPath() 方法将视频文件的路径设置为 VideoView 的源

setOnPreparedListener() 方法,等待 VideoView 准备好后开始播放视频

出现权限问题:No permission to write to /storage/emulated/0/Download/video.mp4

从 Android 6.0(API 级别 23)开始,应用程序需要在运行时获得访问外部存储设备的权限。

1
2
3
4
5
6
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE)
}

如果Activity中使用,将 this 替换为Activity。如果在片段中使用,将 this 替换为 requireActivity()。如果在服务(Service)中使用,则需要传递一个上下文对象。

MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: 自定义的常量,用于标识请求 WRITE_EXTERNAL_STORAGE 权限的权限请求,该值可以是任何整数,只要与应用程序中其他权限请求不冲突即可。

判断是否获得权限

上面的代码请求了 WRITE_EXTERNAL_STORAGE 权限。当用户响应权限请求时,onRequestPermissionsResult() 方法将被调用,在此方法中检查用户是否授予了所需的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> {
// 如果请求被取消,结果数组将为空
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予,执行写入外部存储设备的操作
} else {
// 权限被拒绝,向用户解释为什么需要这个权限
}
return
}
}
}

实例代码

有关数据库的连接参见前面文章

Base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
ini_set('memory_limit', '512M');

$json = file_get_contents('php://input');
$data = json_decode($json, true);
$id = $data['id'];

// 连接到MySQL数据库
$servername = "localhost";
$username = "root";
$password = "123456";
$dbname = "mydatabase";

$conn = new mysqli($servername, $username, $password, $dbname);

// 从MySQL表中获取包含图像路径的记录
$sql = "SELECT video FROM video WHERE id = '$id'";
$result = $conn->query($sql);
$row = $result->fetch_assoc();

$video_path = $row["video"];
$type = pathinfo($video_path, PATHINFO_EXTENSION);
$data = file_get_contents($video_path);
$base64 = base64_encode($data);

//分块
$size = 1048576;
$chunks = str_split($base64, $size);

foreach($chunks as $chunk){
echo $chunk;
flush();
ob_flush();
}

//$response["video_base64"] = $base64;
//echo json_encode($response);

// 关闭数据库连接
$conn->close();
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CoroutineScope(Dispatchers.IO).launch {
val url = URL("http://……/…….php")// 初始化一个URL对象,使用服务器URL。
val connection = url.openConnection() as HttpURLConnection // 使用HttpURLConnection类打开与服务器的连接。
connection.requestMethod = "GET" // 设置请求方法为GET。
connection.doOutput = true // 表示该请求将有一个输出。
connection.setRequestProperty("Content-Type", "application/json") // 将请求的内容类型设置为JSON。
var id: Int = 1 // 使用值为1初始化id变量。
val jsonObject = JSONObject() // 初始化一个新的JSON对象。
jsonObject.put("id", id) // 将id键值对添加到JSON对象中。

val outputWriter = OutputStreamWriter(connection.outputStream) // 获取连接的输出流。
outputWriter.write(jsonObject.toString()) // 将JSON对象作为字符串写入输出流中。
outputWriter.flush() // 刷新输出流。
outputWriter.close() // 关闭输出流。

val responseCode = connection.responseCode // 获取连接的响应代码。
if (responseCode == HttpURLConnection.HTTP_OK) { // 检查响应代码是否为HTTP OK(200)。

val inputStream = connection.inputStream // 获取连接的输入流。
val base64 = StringBuilder() // 初始化一个新的StringBuilder对象以存储Base64编码的视频数据。
val buffer = ByteArray(1024) // 初始化一个新的字节数组以存储数据。
var bytesRead: Int // 初始化一个新的bytesRead变量。
while (inputStream.read(buffer).also { bytesRead = it } > 0) { // 逐块读取数据。
base64.append(String(buffer, 0, bytesRead)) // 将每块数据作为字符串追加到StringBuilder对象中。
}
Log.d("MainActivity", "Response received: $base64") // 将响应记录到控制台中。
// 解码并播放视频
val base64String = base64.toString() // 获取Base64字符串。
val data = Base64.decode(base64String, Base64.DEFAULT) // 将Base64字符串解码为字节数组。
val file = File(getExternalFilesDir(null), "video.mp4") // 初始化一个新的File对象以指定本地文件路径。
val output = FileOutputStream(file) // 初始化一个新的FileOutputStream对象以将字节数组写入文件。
output.write(data) // 将字节数组写入文件。
output.close() // 关闭FileOutputStream对象。
val videoView: VideoView = bd.videoView
file.writeBytes(data)
withContext(Dispatchers.Main){
videoView.setVideoPath(file.absolutePath)
videoView.start()
}
}
}
1
2
3
while (inputStream.read(buffer).also { bytesRead = it } > 0) { // 逐块读取数据。
base64.append(String(buffer, 0, bytesRead)) // 将每块数据作为字符串追加到StringBuilder对象中。
}

从连接的输入流中逐块读取视频数据并将其存储在一个StringBuilder对象中,以便后续进行Base64解码和写入本地文件。在循环中,每次从输入流中读取的数据块都将存储在一个指定大小的字节数组中。 inputStream.read(buffer) 将数据块读入缓冲区中,并返回读取的字节数,返回值为-1时表示已到达流的结尾。通过 alsoit 确保读取到的字节数能够赋值给 bytesRead 变量,并且该变量的值在每次循环迭代中都会被更新。

在每次循环迭代中,使用 String(buffer, 0, bytesRead) 将读取的字节数组转换为字符串,追加到StringBuilder对象中。第二个参数 0 表示从缓冲区的第一个字节开始转换,第三个参数 bytesRead 表示只转换缓冲区中的前 bytesRead 个字节。

URL下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//设置权限,没有异常处理
val MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 123

if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE)
}

//下载视频
val downloadUrl = "http://……/……/…….mp4"
val fileName = "video.mp4"
val request = DownloadManager.Request(Uri.parse(downloadUrl))
.setTitle(fileName)
.setDescription("Downloading")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.enqueue(request)

val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/video.mp4"
val videoView = findViewById<VideoView>(R.id.videoView)
videoView.setVideoPath(filePath)
videoView.setOnPreparedListener { mp -> mp.start() }

视频的存储与使用
http://example.com/2023/03/26/视频的存储与使用/
作者
zhanghao
发布于
2023年3月26日
许可协议