HDFS存儲視頻數據,前端完成視頻預覽


在做的項目中,有需求是在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();
}
前端簡單實現(假設已經用表單選擇好了要上傳的文件(實際我們使用antd的Upload組件獲取本地的文件),文件數據存在state中,點擊確定觸發 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,返回視頻進度條對應的內容,做到真正的在線預覽。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM