Exporta archivos a Vimeo en Java con rapidez y eficiencia
El flujo actual de subidas reanudables de Vimeo crea un video mediante
POST /me/videos con upload.approach establecido en
tus y luego envía los bytes a la upload.upload_link devuelta.
Esto reemplaza el ejemplo antiguo de com.clickntap:vimeo:2.0. La implementación siguiente
usa el cliente HTTP de Java y Gson, siguiendo la
guía de subidas de Vimeo,
la implementación de subidas en Python mantenida por Vimeo
y el protocolo tus.
Requisitos previos
Usa JDK 21, una aplicación de Vimeo autorizada para subir archivos
y un token de acceso de usuario con los permisos de subida necesarios. Confirma con Vimeo el
acceso de tu aplicación para realizar subidas y la cuota disponible en tu cuenta. Esta guía no
presupone un plan de pago específico, un tamaño de archivo permitido ni un límite de duración.
Esas condiciones particulares de cada cuenta requieren una comprobación en tiempo real.
Mantén los tokens de acceso en el servidor. La URL de subida devuelta también es confidencial: guárdala de forma privada y no la incluyas en los registros. Completar la transferencia de bytes no confirma que Vimeo haya terminado de transcodificar el video ni que pueda reproducirse públicamente.
Prepara tu entorno de desarrollo
Guarda la siguiente clase Java como VimeoUploader.java. Este ejemplo independiente
fija la versión de Gson en 2.14.0:
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
javac -cp gson-2.14.0.jar VimeoUploader.java
Para Maven, usa com.google.code.gson:gson:2.14.0. La clase no usa ningún SDK no oficial de Vimeo.
Configura y autentica tu aplicación
Proporciona VIMEO_ACCESS_TOKEN mediante la gestión de secretos de tu servidor.
El comando create realiza una solicitud a la API y guarda un archivo de
sesión privado; resume reutiliza esa sesión sin crear otro video.
El hash del archivo impide reanudar la subida con contenido diferente del mismo tamaño.
Mantén el archivo de entrada inmutable durante toda la transferencia.
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"));
}
// A trusted alternate HTTPS origin permits local protocol verification.
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);
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")
.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("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 static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume video-file session-file");
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 {
throw new IllegalArgumentException("Choose create or resume");
}
}
}
El ejemplo del archivo privado está pensado para macOS y Linux. En Windows, configura una ACL restrictiva equivalente antes de guardar la sesión de forma persistente. Protege también el directorio. Reanuda únicamente las sesiones cuyos archivos haya creado tu aplicación; contienen una URL de destino que puede recibir tu video.
Manejo de errores y reintentos
No reintentes create a ciegas: una respuesta perdida puede dejar un video
en Vimeo aunque el cliente haya recibido un error de tiempo de espera agotado. Si el archivo de
sesión queda vacío tras un fallo, comprueba el estado de la cuenta antes de crear un video de
reemplazo. Tras un fallo de transferencia, vuelve a ejecutar resume con
la misma sesión y el archivo de entrada sin cambios. Su solicitud
HEAD lee el desplazamiento confirmado por el servidor, incluidos los
bytes aceptados antes de que se perdiera una respuesta.
Los fallos de autenticación y las solicitudes no válidas requieren correcciones. Respeta
Retry-After de Vimeo o sus indicaciones sobre límites de solicitudes para
regular la frecuencia, y aplica una espera progresiva ante fallos transitorios del servicio o de
la red antes de reanudar. Una URL de subida vencida requiere una recuperación explícita; no la
conviertas en una solicitud automática para crear otro video. El ejemplo nunca adjunta el token
Bearer de Vimeo a la URL de subida del protocolo tus ni cambia la privacidad.
Prueba tu implementación
Tras obtener la autorización de la cuenta, estos comandos crean un video y transfieren sus bytes:
java -cp '.:gson-2.14.0.jar' VimeoUploader create video.mp4 session.json
java -cp '.:gson-2.14.0.jar' VimeoUploader resume video.mp4 session.json
Los entornos de prueba HTTPS locales pueden comprobar la solicitud JSON, los desplazamientos de reanudación, los bytes de los fragmentos, los encabezados ausentes y los estados de error. No pueden confirmar el acceso de tu aplicación para realizar subidas, la cuota, el resultado de la transcodificación ni la reproducción. Verifica esos aspectos con una subida real autorizada expresamente antes de desplegar la implementación.
Buenas prácticas para subir videos
Mantén el video inmutable durante la subida, guarda su URI de forma persistente para consultar su estado más adelante y trata las URL de sesión como credenciales. Consulta la documentación actual de Vimeo sobre cuentas y subidas en lugar de fijar nombres de planes o tamaños máximos de archivo en el código.
Mejora los flujos de trabajo con el Robot de exportación a Vimeo de Transloadit
Usa 🤖 /video/encode para la codificación y la adición de marcas de agua; luego pasa su resultado a 🤖 /vimeo/store para exportarlo. El Robot de exportación no realiza por sí mismo todas las transformaciones de video anteriores.
Conclusión
Crea un solo video en Vimeo, conserva su sesión de subida y reanuda desde el desplazamiento confirmado del protocolo tus. Para obtener procesamiento gestionado antes de exportar, consulta el servicio de encoding de video de Transloadit.
