/** * Test the level of functionality available on this system * * @return String indicating the level of available functionality */ private String testExecLevel() { // Make sure we can start if (ffmpeg == null) { ffmpeg = new FfmpegImpl(get(config, "binaries/transcoding"), get(config, "binaries/metadata")); } return ffmpeg.testAvailability(); }
/** * Wrap a basic conversion used repeatedly during file merges * * @param in: The source file * @param out: The outout file * @return String: FFmpeg's console output * @thorws IOException: if there are file access errors */ private String mergeRender(File in, File out) throws IOException { // Render config List<String> params = new ArrayList(); params.add("-i"); params.add(in.getAbsolutePath()); params.add("-sameq"); params.add("-r"); if (out.getName().equals(MERGED_PAYLOAD + finalFormat)) { params.add(finalRate); } else { params.add(mergeRate); } params.add(out.getAbsolutePath()); // Render return ffmpeg.transform(params, outputDir); }
/** Reset the transformer in preparation for a new object */ private void reset() throws TransformerException { if (firstRun) { firstRun = false; testExecLevel(); // Prep output area String outputPath = config.get("outputPath"); outputRoot = new File(outputPath); outputRoot.mkdirs(); // Database String useDB = config.get("database/enabled", "false"); if (Boolean.parseBoolean(useDB)) { try { stats = new FfmpegDatabase(config); } catch (Exception ex) { log.error("Statistics database failed to initialise!"); } } // Set system variable for presets location String presetsPath = config.get("presetsPath"); if (presetsPath != null) { File presetDir = new File(presetsPath); // Make sure it's valid if (presetDir.exists() && presetDir.isDirectory()) { // And let FFmpeg know about it ffmpeg.setEnvironmentVariable("FFMPEG_DATADIR", presetDir.getAbsolutePath()); } else { log.error("Invalid FFmpeg presets path provided: '{}'", presetsPath); } } } itemConfig = null; info = null; format = null; errors = new LinkedHashMap(); metadata = new LinkedHashMap(); oldMetadata = new LinkedHashMap(); }
/** * Convert audio/video to required output(s) * * @param sourceFile : The file to be converted * @param render : Configuration to use during the render * @param info : Parsed metadata about the source * @return File containing converted media * @throws TransformerException if the conversion failed */ private File convert(File sourceFile, JsonConfigHelper render, FfmpegInfo info) throws TransformerException { // Statistics variables long startTime, timeSpent; String resolution; // One list for all settings, the other is a subset for statistics List<String> statParams = new ArrayList<String>(); List<String> params = new ArrayList<String>(); // Prepare the output location String outputName = render.get("name"); if (outputName == null) { return null; } File outputFile = new File(outputDir, outputName); if (outputFile.exists()) { FileUtils.deleteQuietly(outputFile); } log.info("Converting '{}': '{}'", sourceFile.getName(), outputFile.getName()); // Get metadata ready JsonConfigHelper renderMetadata = new JsonConfigHelper(); String key = jsonKey(outputName); String formatString = render.get("formatMetadata"); if (formatString != null) { renderMetadata.set("format", formatString); } String codecString = render.get("codecMetadata"); if (codecString != null) { renderMetadata.set("codec", codecString); } try { // ************* // 1) Input file // ************* params.add("-i"); params.add(sourceFile.getAbsolutePath()); // Overwrite output file if it exists params.add("-y"); // ************* // 2) Configurable options // ************* String optionStr = render.get("options", ""); List<String> options = split(optionStr, " "); // Replace the offset placeholder now that we know the duration long start = 0; for (int i = 0; i < options.size(); i++) { String option = options.get(i); // For stats, use placeholder.. random data messes with hashing statParams.add(option); // If it even exists that is... if (option.equalsIgnoreCase("[[OFFSET]]")) { start = (long) (Math.random() * info.getDuration() * 0.25); option = Long.toString(start); } // Store the parameter for usage params.add(option); } // ************* // 3) Video resolution / padding // ************* String audioStr = render.get("audioOnly"); boolean audio = Boolean.parseBoolean(audioStr); // Non-audio files need some resolution work if (!audio) { List<String> dimensions = getPaddedParams(render, info, renderMetadata, statParams); if (dimensions == null || dimensions.isEmpty()) { addError(key, "Error calculating dimensions"); return null; } // Merge resultion parameters into standard parameters params.addAll(dimensions); } // Statistics String width = renderMetadata.get("width"); String height = renderMetadata.get("height"); if (width == null || height == null) { // Audio... or an error resolution = "0x0"; } else { resolution = width + "x" + height; } // ************* // 4) Output options // ************* optionStr = render.get("output", ""); options = split(optionStr, " "); // Merge option parameters into standard parameters if (!options.isEmpty()) { params.addAll(options); statParams.addAll(options); } params.add(outputFile.getAbsolutePath()); // ************* // 5) All done. Perform the transcoding // ************* startTime = new Date().getTime(); String stderr = ffmpeg.transform(params, outputDir); timeSpent = (new Date().getTime()) - startTime; renderMetadata.set("timeSpent", String.valueOf(timeSpent)); renderMetadata.set("debugOutput", stderr); if (outputFile.exists()) { long fileSize = outputFile.length(); if (fileSize == 0) { throw new TransformerException("File conversion failed!\n=====\n" + stderr); } else { renderMetadata.set("size", String.valueOf(fileSize)); } } else { throw new TransformerException("File conversion failed!\n=====\n" + stderr); } // log.debug("FFMPEG Output:\n=====\n\\/\\/\\/\\/\n{}/\\/\\/\\/\\\n=====\n", // stderr); } catch (IOException ioe) { addError(key, "Failed to convert!", ioe); throw new TransformerException(ioe); } // On a multi-pass encoding we may be asked to // throw away the video from some passes. if (outputFile.getName().contains("nullFile")) { return null; } else { // For anything else, record metadata metadata.put(key, renderMetadata); // And statistics if (stats != null) { Map<String, String> data = new HashMap(); data.put("oid", oid); data.put("datetime", String.valueOf(startTime)); data.put("timespent", String.valueOf(timeSpent)); data.put("renderString", StringUtils.join(statParams, " ")); data.put("mediaduration", String.valueOf(info.getDuration())); data.put("inresolution", info.getWidth() + "x" + info.getHeight()); data.put("outresolution", resolution); data.put("insize", String.valueOf(sourceFile.length())); data.put("outsize", String.valueOf(outputFile.length())); data.put("infile", sourceFile.getName()); data.put("outfile", outputFile.getName()); try { stats.storeTranscoding(data); } catch (Exception ex) { log.error("Error storing statistics: ", ex); } } } return outputFile; }
/** * 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; }