背景
在使用 巨量引擎 时,我们可能会通过 API 获取到视频的 URL。但是,获取到的视频 URL 通常是 前端临时链接,不能直接用于下载。为了能够下载视频并上传到自己的存储系统,我们需要绕过这个临时链接的限制。
通过分析浏览器的开发者工具(F12),我们发现,虽然视频 URL 是临时的,但它背后存在一个 302 重定向,通过该重定向可以获取到一个有效的、可以直接下载的视频地址。接下来,我们将介绍如何利用这一点来下载视频,并将其上传到 连山 的对象存储。
获取视频的临时链接
首先,我们使用 巨量引擎 API 中的接口,来请求获取视频的 URL。巨量引擎提供了一个接口,能够返回广告视频的相关信息,其中包括视频的 URL。具体接口如下:
该接口返回的数据中,包含了一个临时的 video_url,该链接指向的视频是临时的,因此不能直接下载。

返回的链接类似于:
https://cc.oceanengine.com/anm/CsoDcnZJc1URoiNJCoy7D5Md3NhcNhzAR2gSeMz1-fVCMuR16p1n0gbn5dYUIeWkgXE1zT75mX7Ianl0f0d2msKxKeJH5oACQM2Hn1ixIt2vhQXp0qYQQfSDb9796kZ2TE47Q_RRlpzsb8trRgW2TO1dO6FeD7tk6YdNlD1RXUZ9dX5xTxUkdAaJbB7TCJwHQw2ziUi8p5CR7rPLqZuqVLmtpYSUpVK5M0z3oLqKuU23zRKbrJfa179cUvaZllyZauzlmWUZoJR8CCvRDMreWJkAOte2rDjBUuQBVMTUMiDJLfb1wEVznDnkZk3AYebXG9aQo2sDwf-AJCpIS7I2zRjqXwrLlMbdYNs2Mw9HNY8xKG2wo7v_4oUAo1QPvWUI-nXfF_oh4c9XJRnOnps2rFdm-2oWak43CeeVH66eYGtBtgC0iO6XPFRR0j_zn6V5xgskuKZlNDfdOB-Fju36-L5PdMTydbJbp97SR09Adjnqz35m-qhSP6yOMsg-jglnT9phjY8hU0pBMVnFuuaPIAAoHlkPFa0uaBYe9CjLmQ1yl2T6s7bBKmlAasSVkX7NIWPGJyE7bQUD8B4fdzIyJ93jM2FEwKP567BQLzMaSwo8AAAAAAAAAAAAAE-oNyT7JAzSaiEZbzi1UJv_nlcIyiTUjexlRbi_pBsLgx4aQa9DDhULVrie-fa66ynlEMesgA4Y7fL_4QEgASIBA5x2FaU=是一个前端地址,而且还是加密的,临时的,每次请求都不一样
分析 F12 重定向过程
访问此链接,使用浏览器的开发者工具(F12),我们发现,当我们请求这个临时视频 URL 时,会首先返回一个 302 Redirect 响应。

在响应中,我们看到以下关键内容:
Set-Cookie:返回了视频请求所需的 Cookie。
Location:重定向到一个新的 URL,这个 URL 是实际的视频地址。
从这个过程我们可以了解到,虽然初始的 URL 是临时的,但它会通过重定向获取到一个永久的视频下载地址。

于是我便怀疑这个set-cookie是访问视频的关键信息,因为最开始时访问会被直接返回一个403。
解决方案:解析重定向并下载视频
为了下载视频,我们需要模拟这个过程,首先发送请求到临时的 URL,获取重定向信息(特别是 Set-Cookie 和 Location),然后通过这些信息发送请求到最终的视频 URL,成功获取视频文件。
关键代码实现
以下是使用 OkHttp 发送请求并解析视频下载地址的代码:
import okhttp3.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class VideoDownloader {
private final OkHttpClient sharedClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(150, 30, TimeUnit.MINUTES))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) //优先 HTTP/2
.followRedirects(false) // 禁用自动重定向
.build();
public InputStream downloadFromRealUrl(String initialUrl) throws IOException {
// 第一步:发送请求获取 302 重定向
Request initialRequest = new Request.Builder()
.url(initialUrl)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
.header("Accept", "video/webm,video/ogg,video/*;q=0.9")
.header("Referer", "https://cc.oceanengine.com/")
.header("Origin", "https://cc.oceanengine.com/")
.build();
// 执行请求
Response initialResponse = sharedClient.newCall(initialRequest).execute();
// 处理 302 响应
if (initialResponse.code() == 302) {
// 提取 Set-Cookie 和 Location
String setCookie = initialResponse.header("Set-Cookie");
String location = initialResponse.header("Location");
if (setCookie == null || location == null) {
throw new RuntimeException("响应缺少必要的 Set-Cookie 或 Location");
}
// 第三步:提取到的 cookies 在后续请求中使用
// 创建一个新的请求到重定向的 URL
Request downloadRequest = new Request.Builder()
.url(location)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
.header("Accept", "video/webm,video/ogg,video/*;q=0.9")
.header("Referer", "https://cc.oceanengine.com/")
.header("Origin", "https://cc.oceanengine.com/")
.header("Cookie", setCookie) // 设置从 302 响应中获取的 cookies
.build();
// 执行下载请求
Response downloadResponse = sharedClient.newCall(downloadRequest).execute();
if (!downloadResponse.isSuccessful()) {
throw new RuntimeException("视频下载失败: HTTP " + downloadResponse.code());
}
// 返回视频流
return downloadResponse.body().byteStream();
} else {
throw new RuntimeException("初始请求失败: HTTP " + initialResponse.code());
}
}
}代码解析
禁用自动重定向:我们禁用了
followRedirects(false),这样可以手动处理 302 重定向。获取 Set-Cookie 和 Location:从初次响应中提取
Set-Cookie(存储了视频下载所需的 Cookie)和Location(视频重定向 URL)。通过 Cookies 请求最终视频地址:使用从初始请求中获得的
Set-Cookie,然后发送请求到Location指向的最终视频地址,成功获取视频流。
上传到连山云
通过这种方式,我们成功使用Java解析了视频文件,并可以将其上传到 连山 对象存储桶,因为巨量引擎的上传视频素材只接受连山内网视频地址,因此我们可以直接上传视频流到指定的存储桶,并获取到内网地址,上传视频素材到指定账户上。
// 对象名,在连山云 TOS 中保存的路径(建议使用英文/数字,避免中文)
String objectKey = "video/" + newFileName;
try (
TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY, SECRET_KEY);
InputStream inputStream = downloadFromUrl(videoDownUrl) // 使用增强方法
) {
PutObjectInput putObjectInput = new PutObjectInput()
.setBucket(BUCKET_NAME)
.setKey(objectKey)
.setContent(inputStream);
PutObjectOutput output = tos.putObject(putObjectInput);
// 构造 TOS 公网访问地址
tosUrl = String.format(
"https://%s.tos-%s.ivolces.com/%s",
BUCKET_NAME, REGION, objectKey
);
log.info("视频上传成功!TOS URL: {}", tosUrl);
log.info("ETag: {}, CRC64: {}", output.getEtag(), output.getHashCrc64ecma());
} catch (IOException e) {
log.error("读取视频URL失败,无法下载文件。URL={}", videoDownUrl, e);
record.setResult("读取视频URL失败,无法下载文件:" + e.getMessage());
return record;
} catch (TosClientException e) {
log.error("TOS客户端异常,请求未发送。Bucket={}, Key={}", BUCKET_NAME, objectKey, e);
record.setResult("TOS客户端异常,请求未发送:" + e.getMessage());
return record;
} catch (TosServerException e) {
log.error("TOS服务端返回错误。StatusCode={}, Code={}, Message={}",
e.getStatusCode(), e.getCode(), e.getMessage(), e);
record.setResult("TOS服务端返回错误:状态码=" + e.getStatusCode() + ", 错误码=" + e.getCode() + ", 消息=" + e.getMessage());
return record;
} catch (Throwable t) {
log.error("未知异常发生", t);
record.setResult("未知异常发生:" + t.getMessage());
return record;
}其中的REGION,ENDPOINT,ACCESS_KEY,SECRET_KEY,BUCKET_NAME是全局变量,根据自身情况自行设置
默认评论
Halo系统提供的评论