• 作者:老汪软件技巧
  • 发表时间:2024-09-16 04:01
  • 浏览量:

一、背景

在APP上实现如图功能, 用户下载视频,然后用户授权保存在相册中, 下载时loading,成功失败分别进行提示

二、ios 端视频下载2-1、h5端发送下载视频消息,传入视频地址

    iosBridge.pubSub.publish(messageKey.sendToNative, messageKey.downLoadVideo, {
        videoUrl: videoURL,
    });

2-2、ios 端接收消息,执行下载方法1、视频下载

Ios 端通过封装 VideoDownloader 实例,传入videourl 当下载成功或者失败,通过执行回调函数进行后续操作,后续主要是通过消息通知h5

-(void)downLoadVideo:(NSString *)videoUrl{
    // 开始下载
    [self channelMessage:@"showLoading" withData: @"true"];
    NSLog(@"videoUrl%@",videoUrl);
    // 创建下载器实例
      VideoDownloader *downloader = [[VideoDownloader alloc] init];
      // 要下载的URL
      NSURL *videoURL = [NSURL URLWithString:videoUrl];
      // 开始下载
      [downloader downloadVideoWithURL:videoURL completion:^(NSURL *filePath, NSError *error) {
          if (error) {
              NSLog(@"视频下载失败: %@", error.localizedDescription);
              [self channelMessage:@"showLoading" withData: @"false"];
              [self channelMessage:@"showToast" withData: @"Download failed"];
          } else {
              dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                  [self saveVideoToPhotoLibrary:filePath.path];
              });
          }
      }];
}

2、保存到相册

下载成功后会获取到视频的下载地址 filePath.path 然后通过 【PHPhotoLibrary】 将视频地址路径保存到相册中


//保存到相册
- (void)saveVideoToPhotoLibrary:(NSString *)filePath {
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:filePath]];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        if (success) {
            NSLog(@"视频已保存到相册");
            [self channelMessage:@"showLoading" withData: @"false"];
            [self channelMessage:@"showToast" withData: @"The video has been saved to the photo album"];
        } else {
            [self channelMessage:@"showLoading" withData: @"false"];
            NSLog(@"保存视频到相册失败: %@", error.localizedDescription);
            [self channelMessage:@"showToast" withData: @"Failed to save the video to the photo album"];
        }
    }];
}

3、实现 VideoDownloader

暴露 downloadVideoWithURL 方法,入参为 videoUrl 和 completion 函数,执行后续结果

VideoDownloader.h


#import 
NS_ASSUME_NONNULL_BEGIN
@interface VideoDownloader : NSObject
- (void)downloadVideoWithURL:(NSURL *)url
                  completion:(void (^)(NSURL *filePath, NSError *error))completion;
@end
NS_ASSUME_NONNULL_END

VideoDownloader.m

#import "VideoDownloader.h"
@implementation VideoDownloader
- (void)downloadVideoWithURL:(NSURL *)url
                  completion:(void (^)(NSURL *filePath, NSError *error))completion {
    NSURLSession *session = [NSURLSession sharedSession]; 
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url
                                                       completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        if (error) {
            completion(nil, error);
            return;
        }
        
        // 将下载的文件移到临时目录
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *documentsDirectory = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
        NSURL *destinationURL = [documentsDirectory URLByAppendingPathComponent:[response suggestedFilename]];
        
        // 删除已存在的文件
        if ([fileManager fileExistsAtPath:[destinationURL path]]) {
            [fileManager removeItemAtURL:destinationURL error:nil];
        }
        
        NSError *fileError;
        [fileManager moveItemAtURL:location toURL:destinationURL error:&fileError];
        
        if (fileError) {
            completion(nil, fileError);
        } else {
            completion(destinationURL, nil);
        }
    }];
    
    [downloadTask resume];
}
@end

通过NSURLSessionDownloadTask 执行下载任务

三、android 端视频下载3-1、h5发送下载视频消息,传入视频地址

androidBridge.pubSub.publish(androidBridgeMessage.sendToNative,androidBridgeMessage.downLoadVideo, {
    videoUrl: videoURL
});

3-2、android 端接收消息,执行下载任务

执行 downLoadVideo 方法传入 videoUrl 参数,同样将业务逻辑封装到 DownloadUtil 类中, 在执行时通知 h5 loading, 下载成功或者失败后执行相应的回调函数

@JavascriptInterface
fun downLoadVideo(videoJSON:String) {
    val jsonObj = JSONObject(videoJSON)
    val videoUrl = jsonObj.getString("videoUrl")
    callJsFromAndroid("channelMessage","showLoading", "true")
    Handler().postDelayed({
        DownloadUtil.downloadVideo(this@MainActivity, videoUrl) { success, message ->
            if (success) {
                callJsFromAndroid("channelMessage","showToast", message)
            } else {
                callJsFromAndroid("channelMessage","showToast", message)
            }
        }
    },1000)
}

这里为了防止下载太快,增加了1秒的延时, 为了更好的展示loading

3-3、实现 DownloadUtil.kt 类

DownloadUtil 类中主要通过 DownloadManager 实现videoUrl 的下载,当下载成功后,通知媒体扫描器,已便相册可以扫描到新下载的视频, 通过 DownloadManager.Query().setFilterById 循环检查是否下载成功, 这里通过 handler.postDelayed 优化检查过程, 减少查询频次,否则在下载大视频时,一些安卓机型容易崩溃导致退出,代码如下


class DownloadUtil {
    companion object {
        private val handler = Handler(Looper.getMainLooper())
        private val VIDEO_ID = "7408881885343976710"
        fun downloadVideo(
            context: Context,
            videoUrl: String,
            onDownloadComplete: (Boolean, String?) -> Unit
        ) {
            val downloadManager =
                context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            val request = DownloadManager.Request(Uri.parse(videoUrl))
                .setTitle("Downloading video")
                .setDescription("Downloading video file")
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
                .setDestinationInExternalPublicDir(
                    Environment.DIRECTORY_MOVIES,
                    "video_$VIDEO_ID.mp4"
                )
            val downloadId = downloadManager.enqueue(request)
            queryDownloadStatus(context, downloadId, object : DownloadStatusListener {
                override fun onStatusRetrieved(status: Int) {
                    Log.d("videoFile lll", "onStatusRetrievedkkk:")
                    when (status) {
                        DownloadManager.STATUS_FAILED -> {
                            onDownloadComplete(
                                false,
                                "Failed to save the video to the photo album"
                            )
                        }
                        DownloadManager.STATUS_SUCCESSFUL -> {
                            // 通知媒体扫描器,以便相册可以扫描到新下载的视频
                            val videoFile =
                                File(context.getExternalFilesDir(null), "video_$VIDEO_ID.mp4")
                            MediaScannerConnection.scanFile(
                                context,
                                arrayOf(videoFile.absolutePath),
                                null,
                                null
                            )
                            onDownloadComplete(
                                true,
                                "The video has been saved to the photo album"
                            )
                        }
                        else -> {
                            // 继续检查
                            handler.postDelayed(
                                { queryDownloadStatus(context, downloadId, this) },
                                1000
                            )
                        }
                    }
                }
            })
        }
        }
        }
        private fun queryDownloadStatus(
            context: Context,
            downloadId: Long,
            listener: DownloadStatusListener,
        ) {
            val query = DownloadManager.Query().setFilterById(downloadId)
            val downloadManager =
                context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            downloadManager.query(query).use { cursor ->
                if (cursor.moveToFirst()) {
                    val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
                    val status = cursor.getInt(statusIndex)
                    listener.onStatusRetrieved(status)
                }
            }
        }
    interface DownloadStatusListener {
        fun onStatusRetrieved(status: Int)
}

三、 总结

在app上实现视频下载,思路还是比较清晰的, 如果需要权限,就检查权限,下载视频后,保存到用户相册中, 方便用户查看, ios 上在开发时遇到的问题不多, 主要是用户授权的问题, 当用户授权允许保存到相册时,执行相应的方法, 在android 上开发比较坑的时, 安卓对权限这块版本的变化很快, 开始下载视频不成功,一直以为是权限问题, 在android 低版本确实是需要提示用户请求对应权限,才有写入相册的权限,但是后面高版本逐渐废弃了,可以不通过用户授权也可以下载视频到内部地址,在通过媒体扫描更新相册,用户即可看到下载后的视频内容,由于不是专业端开发人员, 里面还是有很多地方需要补充完善, 比如ios用户拒绝授权,下次点击应该检查权限,再次调用授权弹窗,比如android对于低版本的用户的兼容处理,等等应该还有很多细节, 目前还是先实现功能为主,特此记录