Rapidez e eficiência: exportar arquivos para o Vimeo em Java
Crie um único vídeo privado no Vimeo, salve sua sessão de upload e retome uma transferência interrompida a partir do deslocamento de bytes confirmado pelo Vimeo. O programa Java abaixo usa o cliente HTTP e o Gson para implementar o fluxo de upload retomável do Vimeo, com comandos separados para criação, transferência e consulta do status de processamento.
Pré-requisitos
Este tutorial se destina ao Linux com Bash, cURL e uma distribuição do JDK 21 com manutenção ativa.
Ele foi testado com o OpenJDK 21.0.12.1; use as atualizações de segurança atuais do seu distribuidor para a
linha Java 21 com manutenção ativa. Defina JAVA_HOME como o diretório de instalação
desse JDK. Os comandos selecionam explicitamente o compilador e o ambiente de execução dele.
Você também precisa de uma aplicação Vimeo com acesso a uploads, um token de acesso de usuário que tenha public,
private e upload como escopos e espaço na cota da
conta de destino. Confirme esses pré-requisitos antes de criar um vídeo de teste. Use um vídeo válido,
não vazio e que você tenha permissão para fazer upload, e execute apenas um processo de upload por
vez para o arquivo de sessão dele.
Mantenha os tokens de acesso no servidor. A URL de upload retornada também é sensível: armazene-a de forma privada e não a inclua nos logs. A conclusão da transferência de bytes não comprova que o Vimeo terminou de transcodificar o vídeo nem que ele pode ser reproduzido publicamente.
Configurar seu ambiente de desenvolvimento
Em um diretório de trabalho privado, salve a classe completa abaixo como VimeoUploader.java. Esta
configuração independente baixa o Gson 2.14.0
e compila a classe. Cada comando deve ser concluído com sucesso antes da execução do próximo:
test -x "$JAVA_HOME/bin/java" &&
test -x "$JAVA_HOME/bin/javac" &&
command -v curl >/dev/null &&
curl --fail --location --output gson-2.14.0.jar \
https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar &&
"$JAVA_HOME/bin/javac" -cp gson-2.14.0.jar VimeoUploader.java
Se o download ou a compilação falhar, corrija o problema informado e execute novamente este bloco de configuração. Ele substitui o JAR baixado e recompila a classe; não execute uma classe antiga após uma falha de compilação.
Configurar e autenticar sua aplicação
Forneça VIMEO_ACCESS_TOKEN pelo gerenciamento de segredos do seu servidor. O comando create faz
uma requisição à API e salva um arquivo de sessão privado; resume reutiliza essa sessão sem criar
outro vídeo. O hash do arquivo impede a retomada com conteúdo diferente do mesmo tamanho. Mantenha o
arquivo de entrada imutável durante toda a transferência.
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
public final class VimeoUploader {
public record Session(String uri, String uploadLink, long size, String sha256) {}
private static final Gson JSON = new Gson();
private final HttpClient http;
private final URI api;
public VimeoUploader() {
this(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15))
.followRedirects(HttpClient.Redirect.NEVER).build(), URI.create("https://api.vimeo.com"));
}
// Tests supply a trusted HTTPS receiver instead of the Vimeo API.
VimeoUploader(HttpClient http, URI api) {
this.http = http;
this.api = https(api);
}
private static URI https(URI uri) {
if (!"https".equals(uri.getScheme()) || uri.getHost() == null
|| uri.getRawUserInfo() != null || uri.getFragment() != null) {
throw new IllegalArgumentException("An HTTPS URL without embedded credentials is required");
}
return uri;
}
private static String hash(Path file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (var input = Files.newInputStream(file)) {
byte[] buffer = new byte[65536];
int count;
while ((count = input.read(buffer)) != -1) digest.update(buffer, 0, count);
}
return HexFormat.of().formatHex(digest.digest());
}
public Session create(Path file, String token, String name) throws Exception {
long size = Files.size(file);
if (!Files.isRegularFile(file) || size == 0) throw new IOException("A nonempty file is required");
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
String checksum = hash(file);
JsonObject upload = new JsonObject();
upload.addProperty("approach", "tus");
upload.addProperty("size", size);
JsonObject body = new JsonObject();
body.add("upload", upload);
body.addProperty("name", name);
JsonObject privacy = new JsonObject();
privacy.addProperty("view", "nobody");
body.add("privacy", privacy);
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve("/me/videos"))
.timeout(Duration.ofSeconds(60)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body))).build(),
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo create HTTP " + response.statusCode());
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
String uri = result.get("uri").getAsString();
if (!uri.matches("/videos/[0-9]+")) throw new IOException("Invalid video URI");
URI link = https(URI.create(result.getAsJsonObject("upload").get("upload_link").getAsString()));
return new Session(uri, link.toString(), size, checksum);
}
private static long offset(HttpResponse<?> response, long size) throws IOException {
if (!response.headers().firstValue("Tus-Resumable").orElse("").equals("1.0.0")) {
throw new IOException("Unexpected tus protocol version");
}
String value = response.headers().firstValue("Upload-Offset")
.orElseThrow(() -> new IOException("Missing upload offset"));
long offset;
try { offset = Long.parseLong(value); }
catch (NumberFormatException error) { throw new IOException("Invalid upload offset", error); }
if (offset < 0 || offset > size) throw new IOException("Upload offset out of range");
return offset;
}
public void resume(Path file, Session session) throws Exception {
if (session == null) {
throw new IOException("Missing upload session; reconcile your Vimeo account before creating another video");
}
if (Files.size(file) != session.size() || !hash(file).equals(session.sha256())) {
throw new IOException("The upload file changed");
}
URI link = https(URI.create(session.uploadLink()));
HttpResponse<Void> head = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofSeconds(30)).header("Tus-Resumable", "1.0.0")
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build(),
HttpResponse.BodyHandlers.discarding());
if (head.statusCode() != 200 && head.statusCode() != 204) {
throw new IOException("Upload HEAD HTTP " + head.statusCode());
}
long position = offset(head, session.size());
try (RandomAccessFile input = new RandomAccessFile(file.toFile(), "r")) {
input.seek(position);
byte[] buffer = new byte[1024 * 1024];
while (position < session.size()) {
int length = (int) Math.min(buffer.length, session.size() - position);
input.readFully(buffer, 0, length);
HttpResponse<Void> patch = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofMinutes(2)).header("Tus-Resumable", "1.0.0")
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.header("Upload-Offset", Long.toString(position))
.header("Content-Type", "application/offset+octet-stream")
.method("PATCH", HttpRequest.BodyPublishers.ofByteArray(buffer, 0, length)).build(),
HttpResponse.BodyHandlers.discarding());
if (patch.statusCode() != 204) throw new IOException("Upload PATCH HTTP " + patch.statusCode());
long confirmed = offset(patch, session.size());
if (confirmed != position + length) throw new IOException("Unexpected confirmed offset");
position = confirmed;
}
}
}
public JsonObject status(Session session, String token) throws Exception {
if (session == null || session.uri() == null || !session.uri().matches("/videos/[0-9]+")) {
throw new IOException("Missing or invalid video URI");
}
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve(session.uri()
+ "?fields=uri,upload.status,transcode.status,privacy.view"))
.timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.GET().build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo status HTTP " + response.statusCode());
JsonObject video = JsonParser.parseString(response.body()).getAsJsonObject();
if (!session.uri().equals(video.get("uri").getAsString())) {
throw new IOException("Unexpected status video URI");
}
JsonObject result = new JsonObject();
result.add("uri", video.get("uri"));
result.addProperty("upload", statusValue(video, "upload"));
result.addProperty("transcode", statusValue(video, "transcode"));
result.add("privacy", video.getAsJsonObject("privacy").get("view"));
return result;
}
private static String statusValue(JsonObject video, String field) {
if (!video.has(field) || !video.get(field).isJsonObject()) return "unavailable";
var value = video.getAsJsonObject(field).get("status");
return value == null || value.isJsonNull() ? "unavailable" : value.getAsString();
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume|status video-file session-file");
for (int i = 1; i < args.length; i++) {
if (!args[i].chars().allMatch(c -> c >= 32 && c <= 126)) {
throw new IllegalArgumentException("Use printable ASCII paths for this command-line example");
}
}
Path file = Path.of(args[1]);
Path state = Path.of(args[2]);
VimeoUploader uploader = new VimeoUploader();
if (args[0].equals("create")) {
Files.createFile(state, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
Session session = uploader.create(file, System.getenv("VIMEO_ACCESS_TOKEN"), file.getFileName().toString());
Files.writeString(state, JSON.toJson(session));
System.out.println("Created " + session.uri());
} else if (args[0].equals("resume")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
uploader.resume(file, session);
System.out.println("Bytes transferred for " + session.uri());
} else if (args[0].equals("status")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
System.out.println(JSON.toJson(uploader.status(session, System.getenv("VIMEO_ACCESS_TOKEN"))));
} else {
throw new IllegalArgumentException("Choose create, resume, or status");
}
}
}
O arquivo de sessão é criado de forma exclusiva, com permissões apenas para seu proprietário. Um
arquivo existente nunca é sobrescrito por create. Proteja também o diretório dele e retome apenas
sessões criadas pela sua aplicação: as URLs de upload dessas sessões podem receber seu vídeo. Este
exemplo de linha de comando aceita caminhos com caracteres ASCII imprimíveis para evitar ambiguidades
na decodificação dos nomes de arquivo nativos. Renomeie outros caminhos antes de usá-los e coloque
entre aspas os caminhos que contenham espaços ou caracteres do shell.
Tratar erros e novas tentativas
Não repita create às cegas: a perda de uma resposta pode deixar um vídeo no Vimeo mesmo que quem fez
a chamada tenha recebido um erro de tempo limite. Se o arquivo de sessão estiver vazio após uma
falha, verifique e reconcilie o estado da conta antes de criar um substituto. Após uma falha de
transferência, execute novamente resume com a mesma sessão e a entrada inalterada. A requisição
HEAD lê o deslocamento confirmado pelo servidor, incluindo os bytes aceitos antes da perda de uma resposta.
Falhas de autenticação e requisições inválidas precisam ser corrigidas. Para limitações de
requisições à API, siga as orientações de limite de requisições do Vimeo e o cabeçalho de
resposta X-RateLimit-Reset. Aguarde um intervalo antes de retomar após falhas transitórias do serviço ou da rede.
Uma URL de upload expirada exige recuperação explícita; não transforme isso em uma requisição
automática para criar outro vídeo. O exemplo anexa o token bearer apenas às requisições à API e
solicita privacy.view: nobody ao criar o vídeo. Ele não publica o vídeo. Seus blocos de 1 MiB facilitam a interrupção
do exemplo; para uploads maiores, consulte as recomendações de tamanho de bloco do Vimeo antes de
ajustar a taxa de transferência.
Testar sua implementação
Coloque seu vídeo em video.mp4 ou substitua esse caminho em cada comando. Com VIMEO_ACCESS_TOKEN
disponível no ambiente, crie o vídeo privado uma única vez:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader create "video.mp4" "session.json"
Em seguida, transfira os bytes dele. Se o processo parar ou uma requisição falhar, execute novamente este mesmo comando com o mesmo arquivo e a mesma sessão:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader resume "video.mp4" "session.json"
A mensagem de sucesso identifica o vídeo cujos bytes foram transferidos. Cada retomada começa com
uma requisição HEAD do tus contendo os dois cabeçalhos da tabela de HEAD do Vimeo: Tus-Resumable: 1.0.0 e
Accept: application/vnd.vimeo.*+json;version=3.4. PATCH envia apenas os bytes após o deslocamento confirmado.
O exemplo também envia Accept em PATCH, conforme as instruções de retomada do Vimeo.
Verifique o vídeo de destino separadamente:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader status "video.mp4" "session.json"
Isso imprime um retrato do estado em JSON com a URI do vídeo e os valores de upload, transcode e privacy
fornecidos pelo Vimeo. Um status de upload complete registra a aceitação pelo provedor; um status de
transcodificação complete registra a conclusão do processamento. Nenhum deles torna esse vídeo privado
reproduzível publicamente. Repita status mais tarde se o processamento ainda estiver em andamento.
As requisições atingem o tempo limite enquanto aguardam os cabeçalhos de resposta após 60 segundos
para criação, 30 segundos para HEAD ou consulta de status e dois minutos por PATCH. A leitura do
corpo de uma resposta pode demorar mais do que esse tempo limite. O programa não tem prazo máximo
para o upload inteiro nem um ciclo automático de novas tentativas; use um prazo separado para a
tarefa se seu serviço precisar disso.
Boas práticas para uploads de vídeo
Para verificar o conteúdo armazenado, baixe o arquivo de origem original e compare cada byte com sua
entrada. Os links de arquivos da API do Vimeo
exigem os escopos de token public, private e video_files e um plano de conta elegível.
A disponibilidade do original também depende do
armazenamento de arquivos de origem,
que exige um plano pago e deve ser ativado antes do upload. Uma versão transcodificada é um arquivo
diferente e não comprova a correspondência com os bytes originais. Mantenha seu próprio original e
exclua o vídeo privado de teste da sua conta do Vimeo quando terminar os testes.
Aprimorar fluxos de trabalho com o Robot de exportação para o Vimeo da Transloadit
Use 🤖 /video/encode para codificar e aplicar marcas-d’água e, em seguida, passe a saída para 🤖 /vimeo/store para exportar. O Robot de exportação não realiza, por si só, todas as transformações de vídeo anteriores.
Conclusão
Mantenha a sessão salva até verificar o vídeo de destino e concluir qualquer recuperação. Para processamento gerenciado antes da exportação, consulte o serviço de codificação de vídeo da Transloadit.
