/** * Stream data from specified payload into a file in our temp cache. * * @param object: The digital object to use * @param pid: The payload ID to extract * @return File: The cached File * @throws FileNotFoundException: If accessing the cache fails * @throws StorageException: If accessing the object in storage fails * @throws IOException: If the data copy fails */ private File cacheFile(DigitalObject object, String pid) throws FileNotFoundException, StorageException, IOException { // Get our cache location File file = new File(outputDir, pid); FileOutputStream tempFileOut = new FileOutputStream(file); // Get payload from storage Payload payload = object.getPayload(pid); try { // Copy to cache IOUtils.copy(payload.open(), tempFileOut); } catch (IOException ex) { payload.close(); throw ex; } // Close and return payload.close(); tempFileOut.close(); return file; }
/** * Compile all available metadata and add to object. * * <p>Note that a False response indicates data has been written into the 'errors' map and should * be followed by an 'errorAndClose()' call. * * @return: boolean: False if any errors occurred, True otherwise */ private boolean compileMetadata(DigitalObject object) { // Nothing to do if (info == null) { return true; } if (!info.isSupported()) { return true; } // Get base metadata of source JsonConfigHelper fullMetadata = null; try { fullMetadata = new JsonConfigHelper(info.toString()); } catch (IOException ex) { addError("metadata", "Error parsing metadata output", ex); return false; } // Add individual conversion(s) metadata if (!metadata.isEmpty()) { fullMetadata.setJsonMap("outputs", metadata); } // Write the file to disk File metaFile; try { // log.debug("\nMetadata:\n{}", fullMetadata.toString()); metaFile = writeMetadata(fullMetadata.toString()); if (metaFile == null) { addError("metadata", "Unknown error extracting metadata"); return false; } } catch (TransformerException ex) { addError("metadata", "Error writing metadata to disk", ex); return false; } // Store metadata try { Payload payload = createFfmpegPayload(object, metaFile); payload.setType(PayloadType.Enrichment); payload.close(); } catch (Exception ex) { addError("metadata", "Error storing metadata payload", ex); return false; } finally { metaFile.delete(); } // Everything should be fine if we got here return true; }
/** * Read FFMPEG metadata from the object if it exists * * @param object: The object to extract data from */ private void readMetadata(DigitalObject object) { Set<String> pids = object.getPayloadIdList(); if (pids.contains(METADATA_PAYLOAD)) { try { Payload payload = object.getPayload(METADATA_PAYLOAD); JsonConfigHelper data = new JsonConfigHelper(payload.open()); payload.close(); oldMetadata = data.getJsonMap("outputs"); // for (String k : oldMetadata.keySet()) { // log.debug("\n====\n{}\n===\n{}", k, oldMetadata.get(k)); // } } catch (IOException ex) { log.error("Error parsing metadata JSON: ", ex); } catch (StorageException ex) { log.error("Error accessing metadata payload: ", ex); } } }
/** * Transforming digital object method * * @params object: DigitalObject to be transformed * @return transformed DigitalObject after transformation * @throws TransformerException if the transformation fails */ @Override public DigitalObject transform(DigitalObject object, String jsonConfig) throws TransformerException { if (testExecLevel() == null) { log.error("FFmpeg is either not installed, or not executing!"); return object; } // Purge old data reset(); oid = object.getId(); outputDir = new File(outputRoot, oid); outputDir.mkdirs(); try { itemConfig = new JsonConfigHelper(jsonConfig); } catch (IOException ex) { throw new TransformerException("Invalid configuration! '{}'", ex); } // Resolve multi-segment files first mergeRate = get(itemConfig, "merging/mpegFrameRate", "25"); finalRate = get(itemConfig, "merging/finalFrameRate", "10"); finalFormat = get(itemConfig, "merging/finalFormat", "avi"); mergeSegments(object); // Find the format 'group' this file is in String sourceId = object.getSourceId(); String ext = FilenameUtils.getExtension(sourceId); format = getFormat(ext); // Return now if this isn't a format we care about if (format == null) { return object; } // log.debug("Supported format found: '{}' => '{}'", ext, format); // Cache the file from storage File file; try { file = cacheFile(object, sourceId); } catch (IOException ex) { addError(sourceId, "Error writing temp file", ex); errorAndClose(object); return object; } catch (StorageException ex) { addError(sourceId, "Error accessing storage data", ex); errorAndClose(object); return object; } if (!file.exists()) { addError(sourceId, "Unknown error writing cache: does not exist"); errorAndClose(object); return object; } // ************************************************************** // From here on we know (assume) that we SHOULD be able to support // this object, so errors can't just throw exceptions. We should // only return under certain circumstances (ie. not just because // one rendition fails), and the object must get closed. // ************************************************************** // Read any pre-existing rendition metadata from previous tranformations // ++++++++++++++++++++++++++++ // TODO: This is useless until the last modified date can be retrieved // against the source file. Storage API does not currently support this, // it just returns a data stream. // // Once this feature exists the basic algorithm should be: // 1) Retrieve old metadata // 2) Loop through each rendition preparation as normal // 3) When the transcoding is ready to start, use the parameters to // query the database for the last time the exact transcoding was // and comparing against last modifed. // 4) If the transcoding is newer than the source, skip running FFmpeg // and just use the same metadata as last time. // ++++++++++++++++++++++++++++ // readMetadata(object); // Check for a custom display type String display = get(itemConfig, "displayTypes/" + format); if (display != null) { try { Properties prop = object.getMetadata(); prop.setProperty("displayType", display); prop.setProperty("previewType", display); } catch (StorageException ex) { addError("display", "Could not access object metadata", ex); } } // Gather metadata try { info = ffmpeg.getInfo(file); } catch (IOException ex) { addError("metadata", "Error accessing metadata", ex); errorAndClose(object); return object; } // Can we even process this file? if (!info.isSupported()) { closeObject(object); return object; } // What conversions are required for this format? List<JsonConfigHelper> conversions = getJsonList(itemConfig, "transcodings/" + format); for (JsonConfigHelper conversion : conversions) { String name = conversion.get("alias"); // And what/how many renditions does it have? List<JsonConfigHelper> renditions = conversion.getJsonList("renditions"); if (renditions == null || renditions.isEmpty()) { addError( "transcodings", "Invalid or missing transcoding data:" + " '/transcodings/" + format + "'"); } else { // Config look valid, lets give it a try // log.debug("Starting renditions for '{}'", name); for (JsonConfigHelper render : renditions) { File converted = null; // Render the output try { converted = convert(file, render, info); } catch (Exception ex) { String outputFile = render.get("name"); if (outputFile != null) { addError(jsonKey(outputFile), "Error converting file", ex); } else { // Couldn't read the config for a name addError("unknown", "Error converting file", ex); } } // Now store the output if valid if (converted != null) { try { Payload payload = createFfmpegPayload(object, converted); // TODO: Type checking needs more work // Indexing fails silently if you add two thumbnails // or two previews payload.setType(resolveType(render.get("type"))); payload.close(); } catch (Exception ex) { addError(jsonKey(converted.getName()), "Error storing output", ex); } finally { converted.delete(); } } } } } // Write metadata to storage if (compileMetadata(object)) { // Close normally closeObject(object); } else { // Close with some errors errorAndClose(object); } // Cleanup if (file.exists()) { file.delete(); } return object; }