首页 > 其他 > 详细

HDFS存储视频数据,前端完成视频预览

时间:2021-03-27 16:33:36      阅读:67      评论:0      收藏:0      [点我收藏+]

在做的项目中,有需求是在HDFS文件系统中支持管理视频数据,并在前端支持在线预览。其中后端使用的是Spring boot框架,前端是React框架。

总体分析

大概需要实现以下的核心功能

  • 视频上传
  • 视频下载
  • 视频预览

一.视频上传

后端提供一个上传接口,将二进制流存到HDFS中。这里使用的是 MultipartFile,直接接受一个二进制流。

前端在用户点击上传按钮的时候,把要上传的文件转化为二进制流并调用后端接口

后端简单实现

@PostMapping("/file")
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file,
                                        @RequestParam("dirPath") String dirPath) {
    return uploadSingleFile(file, dirPath);
}

public ResponseEntity<String> uploadSingleFile(MultipartFile file, String dirPath) {
    String fileName = file.getOriginalFilename();
    String filePath = dirPath + "/" + fileName;
    try {
        uploadFileToHdfs(file, dirPath);
        return new ResponseEntity<>(HttpStatus.OK);
    } catch (PathNotFoundException | PathIsDirectoryException e) {
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    } catch (IOException e) {
        logger.error("Upload " + filePath + " failed.", e);
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

public void uploadFileToHdfs(MultipartFile file, String dirPath) throws IOException {
    // 连接hdfs,实际项目中需要放到构造函数中,防止重复连接
    conf = new Configuration();
    conf.set("dfs.client.use.datanode.hostname", "true");
    conf.set("fs.defaultFS", hdfs://mirage-cluster-01:8020);
    fs = FileSystem.get(conf);

    String fileName = file.getOriginalFilename();
    // 这里只是简单实现,需要调用hdfs接口判断 文件是否已经存在/文件夹路径是否存在等等
    Path fullDirPath = new Path("hdfs://mirage-cluster-01:8020/dev" + dirPath));
    Path fullFilePath = new Path(fullDirPath.toString() + "/" + fileName);
    FSDataOutputStream outputStream = fs.create(fullFilePath);
    outputStream.write(file.getBytes());
    outputStream.close();
}
前端简单实现(假设已经用表单选择好了要上传的文件,点击确定触发 handleOnOk() 函数)

handleOnOk = async e => {
    // 假设已经验证过没有同名文件/文件夹
    // 选择的参数存在了组件的state中
    const { fileName, path, uploadFile } = this.state;

    // 验证上传文件是否为空
    if (uploadFile === null) {
        message.error(‘请先选择上传的文件‘);
        this.setState({
            confirmLoading: false,
        })
        return;
    }

    // 构造上传文件表单
    let file = null;
    if (fileName !== ‘‘) {
        file = new File([uploadFile],
            fileName,
            {
                ‘type‘: uploadFile.type,
            });
    }
    const formData = new FormData();
    formData.append(‘file‘, file);
    formData.append(‘dirPath‘, path);

    // 发送请求
    const init = {
        method: ‘POST‘,
        mode: ‘cors‘,
        body: formData,
    }
    const url = `http://ip:port/file`; //后端接口地址
    const response = await fetch(url, init);
    ... // 下面根据response的状态码判断是否上传成功
}

二. 视频下载

后端提供一个下载接口,直接返回一个二进制流。

前端点击下载按钮,调用接口,下载文件到本地。

后端简单实现

@GetMapping("/file")
public ResponseEntity<InputStreamResource> download(@RequestParam("filePath") String filePath) {
    return downloadSingleFile(filePath);
}

public ResponseEntity<InputStreamResource> downloadSingleFile(String filePath) {
    // 假设已经做完了路径异常判断
    String fileName = pathSplit[pathSplit.length - 1];
    try {
        InputStream in = getHdfsFileInputStream(filePath);
        InputStreamResource resource = new InputStreamResource(in);

        // 设置一些协议参数
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"))
                .body(resource);
    } catch {
        ...
    }
}

/**
* 获取 HDFS 文件的 IO 流
*/
public InputStream getHdfsFileInputStream(@NotNull String filePath) throws IOException {
    // 连接hdfs,实际项目中需要放到构造函数中,防止重复连接
    conf = new Configuration();
    conf.set("dfs.client.use.datanode.hostname", "true");
    conf.set("fs.defaultFS", hdfs://mirage-cluster-01:8020);
    fs = FileSystem.get(conf);

    // 读取文件流
    Path fullFilePath = new Path("hdfs://mirage-cluster-01:8020/dev" + filePath));
    InputStream in = fs.open(fullFilePath);
    return in;
}
前端简单实现 (假设已经选中了要下载的文件,点击按钮触发了 handleOnOk() 函数)

handleOnOK = async (e) => {
    e.stopPropagation();

    const { filePathUpload, file } = this.props;
    const { name, path } = file;

    // 直接请求后端接口
    const url = `http://ip:port/file?filePath=${path}`;
    const response = await fetchTool(url, init);

    if (response && response.status === 200) {
        // 读取文件流
        const data = await response.blob();
        // 创建下载链接
        let blobUrl = window.URL.createObjectURL(data);
        // 创建一个a标签用于下载
        const aElement = document.createElement(‘a‘);
        document.body.appendChild(aElement);
        aElement.style.display = ‘none‘;
        aElement.href = blobUrl;
        // 设置下载后文件名
        aElement.download = name;
        // 触发点击链接,开始下载
        aElement.click();
        // 下载完成,移除对象
        document.body.removeChild(aElement);
    }
}

三.视频预览

后端提供一个预览接口,返回文件流。

前端通过video标签播放。

后端简单实现

@GetMapping("/video-preview")
public ResponseEntity<InputStreamResource> preview(@RequestParam String filePath,
                                                @RequestHeader String range) {

    // 前端使用video标签发起的请求会在header里自带的range参数,对应视频进度条请求视频内容                                                
    return videoPreview(filePath, range);
}

public ResponseEntity<InputStreamResource> videoPreview(String filePath, String range) {
    try {
        // 获取文件流      
        InputStream in = getHdfsFileInputStream(filePath);
        InputStreamResource resource = new InputStreamResource(in);

        // 计算一些需要在response里设置的参数
        long fileLen = getHdfsFileStatus(filePath).getLen();
        long videoRange = Long.parseLong(range.substring(range.indexOf("=") + 1, range.indexOf("-")));
        String[] pathSplit = filePath.split("/");
        String fileName = pathSplit[pathSplit.length - 1];

        // 这里设置的参数都很关键,不然前端播放视频不能拖动进度条
        return ResponseEntity.ok()
                .header("Content-type","video/mp4")
                .header("Content-Disposition", "attachment; filename="+fileName)
                .header("Content-Range", String.valueOf(videoRange + (fileLen-1)))
                .header("Accept-Ranges", "bytes")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .contentLength(fileLen)
                .body(resource);
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}
前端简单实现(假设点击播放按钮,在一个Modal内部,生成播放视频的代码)

buildVideoShowData = () => {
    const { file } = this.props;
    const { path } = file;
    const url = `http://ip:port/video-preview?filePath=${path}`;
    return (
        <video width="840" height="630"
                controls=‘controls‘
                preload=‘auto‘
                autoPlay={true}
                loop={true}
        >
            <source src={url} type="video/mp4"/>
        </video>
    )
}

如果前端播放的video的src是一个指向视频文件的路径,比如将一些视频存放在部署了前端的同一台服务器的本地硬盘上,src=‘./video/xxx.mp4‘。这样的话不需要后端接口,可以在前端直接播放,并且可以拖动视频进度条控制进度。

但这样相当于把视频在前端写死,如果要支持播放用户上传的视频,就不好搞。

所以提供了后端接口从HDFS中读文件流,并将src设置为接口地址获取文件流。在我一开始写这个后端接口时,并没有设置好这些header参数,导致前端播放视频时无法拖动进度条,只能从头往后看。在设置了这些参数之后,后端就可以根据前端传来的视频的range,返回视频进度条对应的内容,做到真正的在线预览。

HDFS存储视频数据,前端完成视频预览

原文:https://www.cnblogs.com/yanch01/p/14585297.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!