Rápido y eficiente: exporta archivos a Vimeo en Java
Crea un único video privado en Vimeo, guarda su sesión de subida y reanuda una transferencia interrumpida desde el desplazamiento en bytes confirmado por Vimeo. El programa Java que aparece más adelante usa el cliente HTTP y Gson para implementar el flujo de subida reanudable de Vimeo, con comandos separados para la creación, la transferencia y el estado del procesamiento.
Requisitos previos
Este tutorial está dirigido a Linux con Bash, cURL y una distribución de JDK
21 con mantenimiento. Se probó con OpenJDK
21.0.12.1; usa las actualizaciones de seguridad actuales de tu distribuidor para la
rama de Java 21 con mantenimiento. Establece
JAVA_HOME en el directorio de instalación de ese JDK. Los comandos seleccionan
explícitamente su compilador y su entorno de ejecución.
También necesitas una aplicación de Vimeo con acceso para subir archivos, un token de acceso de
usuario con public,
private y upload como
ámbitos, y espacio disponible en la cuota de la cuenta receptora.
Confirma esos requisitos antes de crear un video de prueba. Usa un video válido, no vacío y que
tengas permiso para subir, y ejecuta un solo proceso de subida a la vez con su archivo de sesión.
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. Que la transferencia de bytes haya finalizado no demuestra que Vimeo haya terminado de transcodificar el video ni que este se pueda reproducir públicamente.
Configura tu entorno de desarrollo
En un directorio de trabajo privado, guarda la clase completa que aparece más adelante como
VimeoUploader.java. Esta configuración independiente descarga
Gson 2.14.0 y compila la clase. Cada comando debe completarse
correctamente antes de ejecutar el siguiente:
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
Si la descarga o la compilación falla, resuelve el problema indicado y vuelve a ejecutar este bloque de configuración. Reemplaza el JAR descargado y vuelve a compilar la clase; no ejecutes una clase anterior después de una compilación fallida.
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 transferencia con contenido diferente del mismo tamaño. Mantén el archivo de entrada
sin cambios 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"));
}
// 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");
}
}
}
El archivo de sesión se crea de forma exclusiva con permisos solo para su propietario.
create nunca sobrescribe un archivo existente. Protege también su directorio
y reanuda únicamente sesiones creadas por tu aplicación: sus URL de subida pueden recibir tu video.
Este ejemplo de línea de comandos acepta rutas con caracteres ASCII imprimibles para evitar
ambigüedades al decodificar nombres de archivo nativos. Cambia las demás rutas antes de usarlas y
escribe entre comillas las que contengan espacios o caracteres del shell.
Gestión de errores y reintentos
No reintentes create a ciegas: una respuesta perdida puede dejar un video en
Vimeo aunque quien hizo la solicitud haya recibido un error de tiempo de espera agotado. Si el
archivo de sesión está vacío después de un fallo, verifica y concilia el estado de la cuenta antes
de crear un reemplazo. Después de 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. Si la API limita
las solicitudes, sigue las indicaciones de Vimeo sobre límites de solicitudes
y el encabezado de respuesta X-RateLimit-Reset. Espera antes de reanudar cuando haya
fallos transitorios del servicio o de la red.
Una URL de subida caducada requiere una recuperación explícita; no la conviertas en una solicitud
automática para crear un video nuevo. El ejemplo adjunta el token Bearer solo a las solicitudes a
la API y solicita privacy.view: nobody al crear el video. No publica el video. Sus bloques
de 1 MiB facilitan la interrupción del ejemplo; para subidas más grandes, revisa las recomendaciones
de Vimeo sobre el tamaño de los bloques antes de ajustar la velocidad de transferencia.
Prueba tu implementación
Coloca tu video en video.mp4 o sustituye esa ruta por la suya en cada comando.
Con VIMEO_ACCESS_TOKEN disponible en el entorno, crea el video privado una sola vez:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader create "video.mp4" "session.json"
Luego transfiere sus bytes. Si el proceso se detiene o una solicitud falla, vuelve a ejecutar este mismo comando con el mismo archivo y la misma sesión:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader resume "video.mp4" "session.json"
El mensaje de éxito identifica el video cuyos bytes se transfirieron. Cada reanudación comienza con
una solicitud HEAD del protocolo tus que contiene ambos encabezados de la tabla HEAD de Vimeo:
Tus-Resumable: 1.0.0 y
Accept: application/vnd.vimeo.*+json;version=3.4. PATCH envía solo los bytes posteriores al desplazamiento confirmado.
El ejemplo también envía Accept en PATCH, como se muestra en las instrucciones
de reanudación de Vimeo.
Comprueba por separado el video receptor:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader status "video.mp4" "session.json"
Esto imprime una instantánea JSON con el URI del video y los valores de
upload, transcode y privacy
proporcionados por Vimeo. Un estado de subida de complete registra la
aceptación del proveedor; un estado de transcodificación de complete registra
la finalización del procesamiento. Ninguno permite reproducir públicamente este video privado.
Repite status más tarde si el procesamiento sigue en curso. Las solicitudes
agotan el tiempo de espera de los encabezados de respuesta después de 60 segundos para la creación,
30 segundos para HEAD o el estado, y dos minutos por PATCH. La lectura del cuerpo de una respuesta
puede tardar más que ese tiempo de espera. El programa no tiene un plazo máximo para la subida
completa ni un bucle de reintentos automáticos; usa un plazo máximo independiente para la tarea si
tu servicio lo necesita.
Buenas prácticas para subir videos
Para comprobar el contenido almacenado, descarga el archivo fuente original y compara cada byte con
tu archivo de entrada. Los enlaces a archivos de la API de Vimeo
requieren los ámbitos de token public, private y
video_files, así como un plan de cuenta que cumpla los requisitos.
La disponibilidad del original también depende del
almacenamiento de archivos fuente,
que requiere un plan de pago y debe habilitarse antes de subir el archivo. Una variante
transcodificada es un archivo diferente y no permite determinar si sus bytes coinciden con los del
original. Conserva tu propio original y elimina el video privado de prueba de tu cuenta de Vimeo
cuando termines las pruebas.
Mejora los flujos de trabajo con el Robot de exportación a Vimeo de Transloadit
Usa 🤖 /video/encode para el encoding y la aplicación de marcas de agua; luego pasa su salida a 🤖 /vimeo/store para la exportación. El Robot de exportación no realiza por sí mismo todas las transformaciones de video anteriores.
Conclusión
Conserva la sesión guardada hasta que hayas comprobado el video receptor y terminado cualquier recuperación. Para un procesamiento gestionado antes de la exportación, consulta el servicio de encoding de video de Transloadit.
