diff --git a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java index 88f6299..7c599ba 100644 --- a/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java +++ b/src/main/com/portalmedia/embarc/parser/mxf/MXFServiceImpl.java @@ -8,8 +8,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -18,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Set; +import java.util.SortedMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,7 +31,10 @@ import com.portalmedia.embarc.parser.MetadataColumnDef; import com.portalmedia.embarc.parser.StringMetadataColumn; +import tv.amwa.maj.constant.CommonConstants; import tv.amwa.maj.exception.PropertyNotPresentException; +import tv.amwa.maj.industry.PropertyValue; +import tv.amwa.maj.industry.Warehouse; import tv.amwa.maj.io.mxf.BodyPartition; import tv.amwa.maj.io.mxf.BodyPartitionPack; import tv.amwa.maj.io.mxf.EssencePartition; @@ -37,16 +43,24 @@ import tv.amwa.maj.io.mxf.HeaderMetadata; import tv.amwa.maj.io.mxf.HeaderPartitionPack; import tv.amwa.maj.io.mxf.IndexTableSegment; +import tv.amwa.maj.io.mxf.MXFBuilder; +import tv.amwa.maj.io.mxf.MXFConstants; import tv.amwa.maj.io.mxf.MXFFactory; import tv.amwa.maj.io.mxf.MXFFile; import tv.amwa.maj.io.mxf.MXFStream; import tv.amwa.maj.io.mxf.MXFStream.KeyAndConsumed; import tv.amwa.maj.io.mxf.MXFStream.LengthAndConsumed; +import tv.amwa.maj.io.mxf.Partition; +import tv.amwa.maj.io.mxf.PartitionPack; +import tv.amwa.maj.io.mxf.PrimerPack; import tv.amwa.maj.io.mxf.RandomIndexItem; +import tv.amwa.maj.io.mxf.RandomIndexPack; +import tv.amwa.maj.io.mxf.UL; import tv.amwa.maj.io.mxf.impl.EssenceElementImpl; import tv.amwa.maj.io.mxf.impl.FooterPartitionImpl; import tv.amwa.maj.io.mxf.impl.GenericStreamPartitionPackImpl; import tv.amwa.maj.io.mxf.impl.HeaderPartitionImpl; +import tv.amwa.maj.io.mxf.impl.MXFFileImpl; import tv.amwa.maj.io.mxf.impl.RandomIndexItemImpl; import tv.amwa.maj.io.mxf.impl.RandomIndexPackImpl; import tv.amwa.maj.model.AAFFileDescriptor; @@ -82,6 +96,8 @@ import tv.amwa.maj.model.impl.TimedTextDescriptorImpl; import tv.amwa.maj.model.impl.VBIDescriptorImpl; import tv.amwa.maj.model.impl.WAVEPCMDescriptorImpl; +import tv.amwa.maj.meta.ClassDefinition; +import tv.amwa.maj.meta.PropertyDefinition; import tv.amwa.maj.record.AUID; import tv.amwa.maj.record.impl.AUIDImpl; @@ -110,58 +126,136 @@ public MXFFile getFile() { return file; } - private long getFooterOffset(AS07CoreDMSFramework updatedCore) throws IOException { + public Preface getPreface() { MXFFile mxfFile = getFile(); - EmbARCIdentification embarcIdent = getIdentification(); - long totalBytes = 0; - long thisPartition = 0; - long previousPartition = 0; - long klvFillLength = 8192; - ArrayList ripItems = new ArrayList(); - if (!mxfFile.isOpen()) - throw new IOException("Cannot dump file " + filePath + " as the file could not be opened."); - - - HeaderPartitionPack headerPartitionPack = mxfFile.getHeaderPartition().getPartitionPack(); HeaderMetadata headerMetadata = mxfFile.getHeaderPartition().readHeaderMetadata(); - headerPartitionPack.setThisPartition(thisPartition); - headerPartitionPack.setPreviousPartition(previousPartition); - headerPartitionPack.setFooterPartition(0l); - - Preface preface = headerMetadata.getPreface(); + return headerMetadata.getPreface(); + } + /** + * Applies a set of Core DMS column edits onto an existing framework object. + */ + private void applyCoreDMSColumnEdits(AS07CoreDMSFramework dms, HashMap coreColumns) { + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ShimName)) { + dms.setShimName(coreColumns.get(MXFColumn.AS_07_Core_DMS_ShimName).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationName)) { + dms.setResponsibleOrganizationName(coreColumns.get(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationName).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationCode)) { + dms.setResponsibleOrganizationCode(coreColumns.get(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationCode).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_PictureFormat)) { + dms.setPictureFormat(coreColumns.get(MXFColumn.AS_07_Core_DMS_PictureFormat).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Captions)) { + dms.setCaptions(coreColumns.get(MXFColumn.AS_07_Core_DMS_Captions).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_IntendedAFD)) { + dms.setIntendedAFD(coreColumns.get(MXFColumn.AS_07_Core_DMS_IntendedAFD).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackLayout)) { + String value = coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackLayout).toString(); + AudioTrackLayoutValues atlv = new AudioTrackLayoutValues(); + AUID i = AUIDImpl.parseFactory(atlv.getUl(value)); + dms.setAudioTrackLayout(i); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackLayoutComment)) { + dms.setAudioTrackLayoutComment(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackLayoutComment).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackSecondaryLanguage)) { + dms.setAudioTrackSecondaryLanguage(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackSecondaryLanguage).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackPrimaryLanguage)) { + dms.setAudioTrackPrimaryLanguage(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackPrimaryLanguage).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_NatureOfOrganization)) { + dms.setNatureOfOrganization(coreColumns.get(MXFColumn.AS_07_Core_DMS_NatureOfOrganization).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_SecondaryTitle)) { + dms.setSecondaryTitle(coreColumns.get(MXFColumn.AS_07_Core_DMS_SecondaryTitle).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_WorkingTitle)) { + dms.setWorkingTitle(coreColumns.get(MXFColumn.AS_07_Core_DMS_WorkingTitle).toString()); + } + if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Identifiers)) { + IdentifierSetHelper idSetHelper = new IdentifierSetHelper(); + dms.setIdentifiers(idSetHelper.createIdentifierListFromString(coreColumns.get(MXFColumn.AS_07_Core_DMS_Identifiers).toString())); + } + if (coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Devices)) { + DeviceSetHelper deviceSetHelper = new DeviceSetHelper(); + dms.setDevices(deviceSetHelper.createDeviceListFromString(coreColumns.get(MXFColumn.AS_07_Core_DMS_Devices).toString())); + } + } - if(updatedCore!=null) { - updatedCore.setLinkedGenerationID(embarcIdent.getIdentification().getLinkedGenerationID()); + public MXFFileWriteResult writeFile(String outputFilePath, HashMap coreColumns) throws IOException { + return applyCoreDMSEdit(outputFilePath, (marker, dms) -> applyCoreDMSColumnEdits(dms, coreColumns)); + } + + public MXFFileWriteResult writeFile(String outputFilePath, AS07CoreDMSFramework updatedCore) throws IOException { + if (updatedCore == null) { + return copyFileUnchanged(outputFilePath); } - - if(updatedCore!=null) { + return applyCoreDMSEdit(outputFilePath, (marker, dms) -> marker.setDescriptiveFrameworkObject(updatedCore)); + } + + private MXFFileWriteResult copyFileUnchanged(String outputFilePath) { + MXFFileWriteResult result = new MXFFileWriteResult(); + try { + if (!outputFilePath.equals(filePath)) { + Path source = Paths.get(filePath); + Path target = Paths.get(outputFilePath); + if (Files.exists(target)) Files.delete(target); + Files.copy(source, target); + } + result.setSuccess(true); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, ex.toString(), ex); + result.setException(ex); + result.setSuccess(false); + } + return result; + } + + private interface CoreDMSEditor { + void apply(DescriptiveMarker marker, AS07CoreDMSFramework existingFramework); + } + + private static class PartitionEditPlan { + long metadataOffset; + long oldHeaderByteCount; + long newHeaderByteCount; + long delta; + byte[] contentBytes; + } + + private static final long MIN_FILL_SIZE = 20L; + + /** + * Finds the DescriptiveMarker carrying an AS07CoreDMSFramework in the given Preface, or null + * if none is present. + */ + private static DescriptiveMarker findCoreDMSMarker(Preface preface) { + if (preface == null) return null; + try { ContentStorage contentStorage = preface.getContentStorageObject(); Set packages = contentStorage.getPackages(); - - for(tv.amwa.maj.model.Package p : packages) { - if(p instanceof MaterialPackage) { - for(Track t : p.getPackageTracks()) { - if(t instanceof StaticTrack) { - StaticTrack st = (StaticTrack)t; + for (tv.amwa.maj.model.Package p : packages) { + if (p instanceof MaterialPackage) { + for (Track t : p.getPackageTracks()) { + if (t instanceof StaticTrack) { + StaticTrack st = (StaticTrack) t; Segment ts = st.getTrackSegment(); - if(ts instanceof Sequence) { - + if (ts instanceof Sequence) { Sequence seq = (Sequence) ts; - List components = seq.getComponentObjects(); - - for(Component c : components) { - if(c instanceof DescriptiveMarkerImpl) { + for (Component c : seq.getComponentObjects()) { + if (c instanceof DescriptiveMarkerImpl) { DescriptiveMarker m = (DescriptiveMarker) c; try { DescriptiveFramework df = m.getDescriptiveFrameworkObject(); - - if(df!=null && df instanceof AS07CoreDMSFramework) { - m.setDescriptiveFrameworkObject(updatedCore); - } - } - catch(PropertyNotPresentException pnp) { - LOGGER.log(Level.INFO, pnp.toString()); + if (df != null && df instanceof AS07CoreDMSFramework) return m; + } catch (PropertyNotPresentException pnp) { + // not present on this marker, keep looking } } } @@ -170,545 +264,562 @@ private long getFooterOffset(AS07CoreDMSFramework updatedCore) throws IOExceptio } } } + } catch (Exception ex) { + LOGGER.log(Level.INFO, ex.toString()); } - - - int headerSize = (int) (headerPartitionPack.getEncodedSize()); - headerSize = headerSize < 65536 ? 65546 : headerSize; - - try(ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(headerSize)){ - try(ByteArrayOutputStream metadataBytes = new ByteArrayOutputStream(headerSize)){ - - MXFStream.writeHeaderMetadata(metadataBytes, preface); + return null; + } - headerPartitionPack.setHeaderByteCount(metadataBytes.size() + klvFillLength); - - MXFStream.writeFill(metadataBytes, klvFillLength); - - MXFStream.writePartitionPack(headerBytes, headerPartitionPack); - - totalBytes += headerBytes.size(); - totalBytes += metadataBytes.size(); - } + private static long computeNewHeaderByteCount(long contentSize, long oldHeaderByteCount, long kagSize) { + long fillIfSame = oldHeaderByteCount - contentSize; + if (fillIfSame == 0 || fillIfSame >= MIN_FILL_SIZE) { + return oldHeaderByteCount; } - - - RandomIndexItem hRipItem = new RandomIndexItemImpl(headerPartitionPack.getBodySID(), thisPartition); - ripItems.add(hRipItem); - - - EssencePartition hContainerPartition = (EssencePartition)mxfFile.getPartitionAt(0); - if(hContainerPartition!=null) { - for ( EssenceElementImpl element = (EssenceElementImpl) hContainerPartition.readEssenceElement() ; - element != null ; - element = (EssenceElementImpl) hContainerPartition.readEssenceElement() ) { - - - ByteBuffer bb = element.getData(); - byte[] elementByteArray = bb.array(); - - try(ByteArrayOutputStream elementBytes = new ByteArrayOutputStream(elementByteArray.length)){ - MXFStream.writeEssenceElement(elementBytes, element.getEssenceTrackIdentifier(), bb.array()); - totalBytes += elementBytes.size(); - } - - } + long grownTarget = contentSize + calculateKAGFillerSize(contentSize, kagSize); + if (grownTarget <= oldHeaderByteCount) { + grownTarget = oldHeaderByteCount + Math.max(kagSize, MIN_FILL_SIZE); } - for ( int x = 1 ; x < mxfFile.countPartitions()-1; x++ ) { - BodyPartition originalPartition = (BodyPartition)mxfFile.getPartitionAt(x); - BodyPartition partition = originalPartition.clone(); - if(partition!=null) { - - if (partition instanceof FooterPartitionImpl) { - continue; - } - if (partition instanceof HeaderPartitionImpl) continue; - previousPartition = thisPartition; - thisPartition = totalBytes; - - BodyPartitionPack opp = originalPartition.getPartitionPack().clone(); - BodyPartitionPack partitionPack = partition.getPartitionPack().clone(); - - if (partitionPack instanceof GenericStreamPartitionPackImpl) { - GenericStreamPartitionPackImpl gsp = (GenericStreamPartitionPackImpl) partitionPack; - - gsp.setThisPartition(thisPartition); - gsp.setPreviousPartition(previousPartition); - gsp.setFooterPartition(0l); - RandomIndexItem ripItem = new RandomIndexItemImpl(gsp.getBodySID(), thisPartition); - ripItems.add(ripItem); - - int partitionSize = (int) gsp.getEncodedSize(); - partitionSize = partitionSize < 65536 ? 65546 : partitionSize; - - try (ByteArrayOutputStream partitionBytes = new ByteArrayOutputStream(partitionSize)) { - MXFStream.writePartitionPack(partitionBytes, gsp); - IndexTableSegment it = partition.readIndexTableSegment(); - if(it!=null) { - MXFStream.writeIndexTableSegment(partitionBytes, it); - } - totalBytes += partitionBytes.size(); - @SuppressWarnings("unused") - long bytesAdded = ReadGenericStream(opp.getThisPartition() + opp.getEncodedSize() + 20, partitionBytes); - } catch(Exception ex) { - LOGGER.log(Level.WARNING, ex.toString(), ex); - } - - - } else { - partitionPack.setThisPartition(thisPartition); - partitionPack.setPreviousPartition(previousPartition); - partitionPack.setFooterPartition(0l); - RandomIndexItem ripItem = new RandomIndexItemImpl(partitionPack.getBodySID(), thisPartition); - ripItems.add(ripItem); - - int partitionSize = (int) partitionPack.getEncodedSize(); - partitionSize = partitionSize < 65536 ? 65546 : partitionSize; + return grownTarget; + } - try (ByteArrayOutputStream partitionBytes = new ByteArrayOutputStream(partitionSize)) { + private static byte[] buildMetadataRegionBytes(PartitionEditPlan plan) throws IOException { + long fillNeeded = plan.newHeaderByteCount - plan.contentBytes.length; + if (fillNeeded == 0) return plan.contentBytes; + ByteArrayOutputStream out = new ByteArrayOutputStream(plan.contentBytes.length + (int) fillNeeded); + out.write(plan.contentBytes); + MXFStream.writeFill(out, fillNeeded); + return out.toByteArray(); + } - MXFStream.writePartitionPack(partitionBytes, partitionPack); - - IndexTableSegment it = partition.readIndexTableSegment(); - if(it!=null) { - MXFStream.writeIndexTableSegment(partitionBytes, it); - } - totalBytes += partitionBytes.size(); - } catch(Exception ex) { - LOGGER.log(Level.WARNING, ex.toString(), ex); - } - - } - - - EssencePartition containerPartition = (EssencePartition) originalPartition; - if(containerPartition!=null) { - for ( EssenceElementImpl element = (EssenceElementImpl) containerPartition.readEssenceElement() ; - element != null ; - element = (EssenceElementImpl) containerPartition.readEssenceElement() ) { - - - ByteBuffer bb = element.getData(); - byte[] elementByteArray = bb.array(); - - ByteArrayOutputStream elementBytes = new ByteArrayOutputStream(elementByteArray.length); - - MXFStream.writeEssenceElement(elementBytes, element.getEssenceTrackIdentifier(), bb.array()); - totalBytes += elementBytes.size(); - - } - } - } - } - return totalBytes; - + private static byte[] getClassKeyBytes(Class implClass) { + ClassDefinition classDef = Warehouse.lookForClass(implClass); + AUID classAUID = classDef.getAUID(); + ByteBuffer keyBuffer = ByteBuffer.allocate(16); + MXFBuilder.writeKey((UL) classAUID, keyBuffer); + keyBuffer.flip(); + byte[] keyBytes = new byte[16]; + keyBuffer.get(keyBytes); + return keyBytes; } - public Preface getPreface() { - MXFFile mxfFile = getFile(); - HeaderMetadata headerMetadata = mxfFile.getHeaderPartition().readHeaderMetadata(); - - return headerMetadata.getPreface(); + + private static boolean bytesMatchAt(byte[] data, int offset, byte[] pattern) { + if (offset + pattern.length > data.length) return false; + for (int i = 0; i < pattern.length; i++) { + if (data[offset + i] != pattern[i]) return false; + } + return true; } - public MXFFileWriteResult writeFile(String outputFilePath, HashMap coreColumns) throws IOException { - MXFFileWriteResult result = new MXFFileWriteResult(); + + private static long detectPartitionPackPadding(RandomAccessFile source, long offset) throws IOException { + byte[] probe = readRawBytes(source, offset, 32); + ByteBuffer buffer = ByteBuffer.wrap(probe); + UL key; try { - AS07CoreDMSFramework dms = new AS07CoreDMSFrameworkImpl(); - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ShimName)) { - dms.setShimName(coreColumns.get(MXFColumn.AS_07_Core_DMS_ShimName).toString()); - } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationName)) { - dms.setResponsibleOrganizationName(coreColumns.get(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationName).toString()); - } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationCode)) { - dms.setResponsibleOrganizationCode(coreColumns.get(MXFColumn.AS_07_Core_DMS_ResponsibleOrganizationCode).toString()); - } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_PictureFormat)) { - dms.setPictureFormat(coreColumns.get(MXFColumn.AS_07_Core_DMS_PictureFormat).toString()); - } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Captions)) { - dms.setCaptions(coreColumns.get(MXFColumn.AS_07_Core_DMS_Captions).toString()); + key = MXFBuilder.readKey(buffer); + } catch (Exception ex) { + return 0; + } + if (key == null || !MXFBuilder.isKLVFill(key)) return 0; + long length; + try { + length = MXFBuilder.readBERLength(buffer); + } catch (Exception ex) { + return 0; + } + if (length < 0) return 0; + return buffer.position() + length; + } + + private static int[] findTopLevelKlv(byte[] data, byte[] classKeyBytes) { + int limit = data.length; + int position = 0; + while (position + 16 <= limit) { + boolean matches = bytesMatchAt(data, position, classKeyBytes); + ByteBuffer lengthBuffer = ByteBuffer.wrap(data, position + 16, Math.min(9, limit - position - 16)); + int beforePosition = lengthBuffer.position(); + long bodyLength; + try { + bodyLength = MXFBuilder.readBERLength(lengthBuffer); + } catch (Exception ex) { + return null; } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_IntendedAFD)) { - dms.setIntendedAFD(coreColumns.get(MXFColumn.AS_07_Core_DMS_IntendedAFD).toString()); + if (bodyLength < 0 || bodyLength > Integer.MAX_VALUE) return null; + int consumed = lengthBuffer.position() - beforePosition; + int bodyStart = position + 16 + consumed; + int bodyEnd = bodyStart + (int) bodyLength; + if (bodyEnd > limit || bodyEnd < bodyStart) return null; + if (matches) return new int[] { position, bodyStart, bodyEnd }; + position = bodyEnd; + } + return null; + } + + private static int findRealMetadataEnd(byte[] data) { + int limit = data.length; + int position = 0; + int lastRealEnd = 0; + while (position + 16 <= limit) { + UL key; + try { + key = MXFBuilder.readKey(ByteBuffer.wrap(data, position, 16)); + } catch (Exception ex) { + break; } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackLayout)) { - String value = coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackLayout).toString(); - AudioTrackLayoutValues atlv = new AudioTrackLayoutValues(); - AUID i = AUIDImpl.parseFactory(atlv.getUl(value)); - dms.setAudioTrackLayout(i); + ByteBuffer lengthBuffer = ByteBuffer.wrap(data, position + 16, Math.min(9, limit - position - 16)); + int beforePosition = lengthBuffer.position(); + long bodyLength; + try { + bodyLength = MXFBuilder.readBERLength(lengthBuffer); + } catch (Exception ex) { + break; } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackLayoutComment)) { - dms.setAudioTrackLayoutComment(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackLayoutComment).toString()); + if (bodyLength < 0) break; + int consumed = lengthBuffer.position() - beforePosition; + int bodyStart = position + 16 + consumed; + long bodyEndLong = (long) bodyStart + bodyLength; + if (bodyEndLong > limit) break; + int bodyEnd = (int) bodyEndLong; + if (key == null || !MXFBuilder.isKLVFill(key)) { + lastRealEnd = bodyEnd; } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackSecondaryLanguage)) { - dms.setAudioTrackSecondaryLanguage(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackSecondaryLanguage).toString()); + position = bodyEnd; + } + return lastRealEnd; + } + + private static AUID findInstanceUID(byte[] data, int bodyStart, int bodyEnd) { + int position = bodyStart; + while (position + 4 <= bodyEnd) { + int tag = ((data[position] & 0xFF) << 8) | (data[position + 1] & 0xFF); + int length = ((data[position + 2] & 0xFF) << 8) | (data[position + 3] & 0xFF); + int valueStart = position + 4; + int valueEnd = valueStart + length; + if (valueEnd > bodyEnd) break; + if (tag == (MXFConstants.InstanceTag & 0xFFFF) && length == 16) { + byte[] uidBytes = new byte[16]; + System.arraycopy(data, valueStart, uidBytes, 0, 16); + return new AUIDImpl(uidBytes); } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_AudioTrackPrimaryLanguage)) { - dms.setAudioTrackPrimaryLanguage(coreColumns.get(MXFColumn.AS_07_Core_DMS_AudioTrackPrimaryLanguage).toString()); + position = valueEnd; + } + return null; + } + + private static byte[] encodeCoreDMSLocalSet(AS07CoreDMSFramework framework, AUID instanceID, PrimerPack primerPack) + throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { + ClassDefinition classDef = Warehouse.lookForClass(AS07CoreDMSFrameworkImpl.class); + SortedMap properties = classDef.getProperties(framework); + + for (PropertyDefinition property : properties.keySet()) { + if (property.getAUID().equals(CommonConstants.ObjectClassID)) continue; + if (primerPack.lookupLocalTag(property.getAUID()) == null) { + primerPack.addLocalTagEntry(findFreeLocalTag(primerPack), property.getAUID()); } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_NatureOfOrganization)) { - dms.setNatureOfOrganization(coreColumns.get(MXFColumn.AS_07_Core_DMS_NatureOfOrganization).toString()); + } + + ByteArrayOutputStream bodyOut = new ByteArrayOutputStream(); + bodyOut.write(shortToBytes(MXFConstants.InstanceTag)); + bodyOut.write(shortToBytes((short) 16)); + bodyOut.write(instanceID.getAUIDValue()); + + for (PropertyDefinition property : properties.keySet()) { + if (property.getAUID().equals(CommonConstants.ObjectClassID)) continue; + PropertyValue value = properties.get(property); + Short localTag = primerPack.lookupLocalTag(property.getAUID()); + long predictedLength = value.getType().lengthAsBytes(value); + ByteBuffer valueBuffer = ByteBuffer.allocate((int) predictedLength + 64); + value.getType().writeAsBytes(value, valueBuffer); + int actualLength = valueBuffer.position(); + bodyOut.write(shortToBytes(localTag)); + bodyOut.write(shortToBytes((short) actualLength)); + bodyOut.write(valueBuffer.array(), 0, actualLength); + } + + byte[] bodyBytes = bodyOut.toByteArray(); + ByteArrayOutputStream out = new ByteArrayOutputStream(bodyBytes.length + 24); + MXFStream.writeKey(out, (UL) classDef.getAUID()); + MXFStream.writeBERLength(out, bodyBytes.length, 4); + out.write(bodyBytes); + return out.toByteArray(); + } + + private static byte[] shortToBytes(short value) { + return new byte[] { (byte) ((value >> 8) & 0xFF), (byte) (value & 0xFF) }; + } + + private static short findFreeLocalTag(PrimerPack primerPack) throws IOException { + for (int candidate = 0xFFFE; candidate > 0x8000; candidate--) { + short tag = (short) candidate; + if (primerPack.lookupUID(tag) == null) return tag; + } + throw new IOException("Could not find a free local tag to add a new Core DMS property."); + } + + private PartitionEditPlan planPartitionEdit(Partition partition, CoreDMSEditor editor, RandomAccessFile source) + throws IOException, tv.amwa.maj.exception.InsufficientSpaceException { + HeaderMetadata headerMetadata = partition.readHeaderMetadata(); + if (headerMetadata == null) return null; + Preface preface = headerMetadata.getPreface(); + DescriptiveMarker marker = findCoreDMSMarker(preface); + if (marker == null) return null; + + AS07CoreDMSFramework existingFramework = (AS07CoreDMSFramework) marker.getDescriptiveFrameworkObject(); + editor.apply(marker, existingFramework); + + DescriptiveFramework effectiveDf = marker.getDescriptiveFrameworkObject(); + if (!(effectiveDf instanceof AS07CoreDMSFramework)) return null; + AS07CoreDMSFramework effectiveFramework = (AS07CoreDMSFramework) effectiveDf; + EmbARCIdentification embarcIdent = getIdentification(); + effectiveFramework.setLinkedGenerationID(embarcIdent.getIdentification().getLinkedGenerationID()); + + PartitionPack pack = partition.getPartitionPack(); + long partitionPackKlvSize = pack.getEncodedSize() + 20; + long partitionPackEnd = pack.getThisPartition() + partitionPackKlvSize; + long metadataOffset = partitionPackEnd + detectPartitionPackPadding(source, partitionPackEnd); + long oldHeaderByteCount = pack.getHeaderByteCount(); + if (oldHeaderByteCount <= 0 || oldHeaderByteCount > Integer.MAX_VALUE) return null; + + byte[] originalMetadataBytes = readRawBytes(source, metadataOffset, oldHeaderByteCount); + + byte[] frameworkClassKey = getClassKeyBytes(AS07CoreDMSFrameworkImpl.class); + int[] frameworkKlv = findTopLevelKlv(originalMetadataBytes, frameworkClassKey); + if (frameworkKlv == null) return null; + AUID instanceID = findInstanceUID(originalMetadataBytes, frameworkKlv[1], frameworkKlv[2]); + if (instanceID == null) return null; + + PrimerPack primerPack = headerMetadata.getPrimerPack().clone(); + int tagCountBefore = primerPack.countLocalTagEntries(); + byte[] newLocalSetBytes = encodeCoreDMSLocalSet(effectiveFramework, instanceID, primerPack); + boolean primerPackGrew = primerPack.countLocalTagEntries() != tagCountBefore; + + int frameworkStart = frameworkKlv[0]; + int frameworkEnd = frameworkKlv[2]; + int realMetadataEnd = findRealMetadataEnd(originalMetadataBytes); + if (realMetadataEnd < frameworkEnd) realMetadataEnd = frameworkEnd; + + ByteArrayOutputStream contentStream = new ByteArrayOutputStream(originalMetadataBytes.length + newLocalSetBytes.length); + if (primerPackGrew) { + int[] primerKlv = findTopLevelKlv(originalMetadataBytes, getClassKeyBytes(tv.amwa.maj.io.mxf.impl.PrimerPackImpl.class)); + if (primerKlv == null || primerKlv[0] >= frameworkStart) { + throw new IOException("Could not locate the primer pack while extending it for a new Core DMS property."); } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_SecondaryTitle)) { - dms.setSecondaryTitle(coreColumns.get(MXFColumn.AS_07_Core_DMS_SecondaryTitle).toString()); + int newPrimerPackSize = (int) tv.amwa.maj.io.mxf.impl.PrimerPackImpl.lengthAsBytes(primerPack); + ByteBuffer primerBuffer = ByteBuffer.allocate(newPrimerPackSize); + tv.amwa.maj.io.mxf.impl.PrimerPackImpl.writeAsBytes(primerPack, primerBuffer); + + contentStream.write(originalMetadataBytes, 0, primerKlv[0]); + contentStream.write(primerBuffer.array()); + contentStream.write(originalMetadataBytes, primerKlv[2], frameworkStart - primerKlv[2]); + } else { + contentStream.write(originalMetadataBytes, 0, frameworkStart); + } + contentStream.write(newLocalSetBytes); + contentStream.write(originalMetadataBytes, frameworkEnd, realMetadataEnd - frameworkEnd); + byte[] contentBytes = contentStream.toByteArray(); + + PartitionEditPlan plan = new PartitionEditPlan(); + plan.metadataOffset = metadataOffset; + plan.oldHeaderByteCount = oldHeaderByteCount; + plan.newHeaderByteCount = computeNewHeaderByteCount(contentBytes.length, oldHeaderByteCount, pack.getKagSize()); + plan.delta = plan.newHeaderByteCount - oldHeaderByteCount; + plan.contentBytes = contentBytes; + return plan; + } + + private MXFFileWriteResult applyCoreDMSEdit(String outputFilePath, CoreDMSEditor editor) { + MXFFileWriteResult result = new MXFFileWriteResult(); + MXFFile mxfFile = null; + try { + mxfFile = MXFFactory.readPartitions(filePath); + if (!mxfFile.isOpen()) + throw new IOException("Cannot open file " + filePath + " for reading."); + if (mxfFile.getRunInSize() != 0) + throw new IOException("Files with a run-in are not supported by the safe Core DMS rewrite path."); + + PartitionEditPlan headerPlan; + PartitionEditPlan footerPlan; + try (RandomAccessFile source = new RandomAccessFile(filePath, "r")) { + headerPlan = planPartitionEdit(mxfFile.getHeaderPartition(), editor, source); + FooterPartition footerPartition = mxfFile.getFooterPartition(); + footerPlan = footerPartition != null ? planPartitionEdit(footerPartition, editor, source) : null; } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_WorkingTitle)) { - dms.setWorkingTitle(coreColumns.get(MXFColumn.AS_07_Core_DMS_WorkingTitle).toString()); + + if (headerPlan == null && footerPlan == null) { + throw new IOException("Could not locate an AS_07 Core DMS framework to update."); } - if(coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Identifiers)) { - IdentifierSetHelper idSetHelper = new IdentifierSetHelper(); - dms.setIdentifiers(idSetHelper.createIdentifierListFromString(coreColumns.get(MXFColumn.AS_07_Core_DMS_Identifiers).toString())); + + long headerDelta = headerPlan != null ? headerPlan.delta : 0; + long footerDelta = footerPlan != null ? footerPlan.delta : 0; + + if (headerDelta == 0 && footerDelta == 0) { + result = applyInPlace(outputFilePath, headerPlan, footerPlan); + } else { + result = applyWithRelocate(outputFilePath, mxfFile, headerPlan, footerPlan, headerDelta); } - if (coreColumns.containsKey(MXFColumn.AS_07_Core_DMS_Devices)) { - DeviceSetHelper deviceSetHelper = new DeviceSetHelper(); - dms.setDevices(deviceSetHelper.createDeviceListFromString(coreColumns.get(MXFColumn.AS_07_Core_DMS_Devices).toString())); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, ex.toString(), ex); + result.setException(ex); + result.setSuccess(false); + } finally { + if (mxfFile != null) mxfFile.close(); + } + return result; + } + + /** + * Fast path: the new metadata for every changed partition fits within the byte count already + * reserved on disk. Nothing else in the file moves, so only the metadata bytes themselves are + * overwritten via a direct seek + write. + */ + private MXFFileWriteResult applyInPlace(String outputFilePath, PartitionEditPlan headerPlan, PartitionEditPlan footerPlan) { + MXFFileWriteResult result = new MXFFileWriteResult(); + try { + if (!outputFilePath.equals(filePath)) { + Path source = Paths.get(filePath); + Path target = Paths.get(outputFilePath); + if (Files.exists(target)) Files.delete(target); + Files.copy(source, target); } - return writeFile(outputFilePath, dms); - } catch(Exception ex) { + MXFFile outFile = MXFFactory.readPartitions(outputFilePath); + MXFFileImpl outFileImpl = (MXFFileImpl) outFile; + try { + if (headerPlan != null) writePlanInPlace(outFileImpl, headerPlan); + if (footerPlan != null) writePlanInPlace(outFileImpl, footerPlan); + } finally { + outFile.close(); + } + result.setSuccess(true); + } catch (Exception ex) { LOGGER.log(Level.WARNING, ex.toString(), ex); result.setException(ex); result.setSuccess(false); - return result; } + return result; } - public MXFFileWriteResult writeFile(String outputFilePath, AS07CoreDMSFramework updatedCore) throws IOException { + private static void writePlanInPlace(MXFFileImpl mxfFileImpl, PartitionEditPlan plan) throws IOException { + byte[] regionBytes = buildMetadataRegionBytes(plan); + if (regionBytes.length != plan.oldHeaderByteCount) { + throw new IOException("Computed metadata region size does not match the space reserved on disk."); + } + mxfFileImpl.seek(plan.metadataOffset); + ByteBuffer buffer = ByteBuffer.wrap(regionBytes); + while (buffer.hasRemaining()) { + int written = mxfFileImpl.write(buffer); + if (written <= 0) { + throw new IOException("Failed to write the full metadata region in place."); + } + } + } + + /** + * Grow path: at least one partition's new metadata no longer fits in its existing budget. + * Rebuilds the file in a single forward pass, but every byte that is not part of a changed + * metadata region is copied raw (via FileChannel.transferTo) straight from the source file. + * Essence elements and index tables are never parsed, decoded or re-encoded. Every partition + * pack after the resized header is re-serialized only to update its ThisPartition, + * PreviousPartition and FooterPartition offsets (all fixed-size fields, so this never changes + * a partition pack's encoded length); the random index pack's entries are updated the same way. + */ + private MXFFileWriteResult applyWithRelocate(String outputFilePath, MXFFile mxfFile, + PartitionEditPlan headerPlan, PartitionEditPlan footerPlan, long headerDelta) { + MXFFileWriteResult result = new MXFFileWriteResult(); - getFile(); - AS07CoreDMSFramework coreFrameworkHeader = getAS07CoreDMSFrameworkHeader(); - AS07CoreDMSFramework coreFrameworkFooter = getAS07CoreDMSFrameworkFooter(); + String tempFilePath = outputFilePath + ".tmp"; try { - long footerOffset = getFooterOffset(updatedCore); - EmbARCIdentification embarcIdent = getIdentification(); - MXFFile mxfFile = MXFFactory.readPartitions(filePath); - - long totalBytes = 0; - long thisPartition = 0; - long previousPartition = 0; - long klvFillLength = 8192; - ArrayList ripItems = new ArrayList(); - String tempFilePath = outputFilePath + ".tmp"; - try(OutputStream outputStream = new FileOutputStream(tempFilePath)) { - if (!mxfFile.isOpen()) - throw new IOException("Cannot dump file " + filePath + " as the file could not be opened."); - - if(updatedCore!=null) { - updatedCore.setLinkedGenerationID(embarcIdent.getIdentification().getLinkedGenerationID()); - } - - HeaderPartitionPack headerPartitionPack = mxfFile.getHeaderPartition().getPartitionPack(); - HeaderMetadata headerMetadata = mxfFile.getHeaderPartition().readHeaderMetadata(); - - headerPartitionPack.setThisPartition(thisPartition); - headerPartitionPack.setPreviousPartition(previousPartition); - headerPartitionPack.setFooterPartition(footerOffset); - - Preface preface = headerMetadata.getPreface(); - - int headerSize = (int) (headerPartitionPack.getEncodedSize()); - headerSize = headerSize < 65536 ? 65546 : headerSize; + FooterPartition footerPartitionObj = mxfFile.getFooterPartition(); + if (footerPartitionObj == null) { + throw new IOException("Cannot safely relocate a file with no footer partition."); + } + PartitionPack footerPackOriginal = footerPartitionObj.getPartitionPack(); + long oldFooterOffset = footerPackOriginal.getThisPartition(); + long newFooterOffset = oldFooterOffset + headerDelta; - if(updatedCore!=null && coreFrameworkHeader != null) { - ContentStorage contentStorage = preface.getContentStorageObject(); - Set packages = contentStorage.getPackages(); - - for(tv.amwa.maj.model.Package p : packages) { - if(p instanceof MaterialPackage) { - for(Track t : p.getPackageTracks()) { - if(t instanceof StaticTrack) { - StaticTrack st = (StaticTrack)t; - Segment ts = st.getTrackSegment(); - if(ts instanceof Sequence) { - - Sequence seq = (Sequence) ts; - List components = seq.getComponentObjects(); - - for(Component c : components) { - if(c instanceof DescriptiveMarkerImpl) { - DescriptiveMarker m = (DescriptiveMarker) c; - try { - DescriptiveFramework df = m.getDescriptiveFrameworkObject(); - - if(df!=null && df instanceof AS07CoreDMSFramework) { - m.setDescriptiveFrameworkObject(updatedCore); - } - } - catch(PropertyNotPresentException pnp) { - LOGGER.log(Level.INFO, pnp.toString()); - } - } - } - } - } - } - } - } + long originalFileLength = new File(filePath).length(); + RandomIndexPack rip = mxfFile.getRandomIndexPack(); + long ripLength = rip != null ? rip.getLength() : 0; + long oldRipStart = originalFileLength - ripLength; + + try (RandomAccessFile source = new RandomAccessFile(filePath, "r"); + FileOutputStream fos = new FileOutputStream(tempFilePath)) { + + FileChannel srcChannel = source.getChannel(); + WritableByteChannel destChannel = fos.getChannel(); + + // ---- Header partition ---- + PartitionPack headerPackInfo = mxfFile.getHeaderPartition().getPartitionPack(); + long headerPackKlvSize = headerPackInfo.getEncodedSize() + 20; + long headerOldHeaderByteCount = headerPackInfo.getHeaderByteCount(); + long headerPaddingSize = detectPartitionPackPadding(source, headerPackKlvSize); + long headerMetadataOffset = headerPackKlvSize + headerPaddingSize; + + byte[] headerMetadataRegion; + long headerNewHeaderByteCount; + if (headerPlan != null) { + headerNewHeaderByteCount = headerPlan.newHeaderByteCount; + headerMetadataRegion = buildMetadataRegionBytes(headerPlan); + } else { + headerNewHeaderByteCount = headerOldHeaderByteCount; + headerMetadataRegion = readRawBytes(source, headerMetadataOffset, headerOldHeaderByteCount); } - try(ByteArrayOutputStream headerBytes = new ByteArrayOutputStream(headerSize)){ - try(ByteArrayOutputStream metadataBytes = new ByteArrayOutputStream(headerSize)){ - MXFStream.writeHeaderMetadata(metadataBytes, preface); - - headerPartitionPack.setHeaderByteCount(metadataBytes.size() + klvFillLength); - - MXFStream.writeFill(metadataBytes, klvFillLength); - - MXFStream.writePartitionPack(headerBytes, headerPartitionPack); - - - headerBytes.writeTo(outputStream); - metadataBytes.writeTo(outputStream); - totalBytes += headerBytes.size(); - totalBytes += metadataBytes.size(); - } + byte[] headerPackBytes = patchPartitionPackOffsets(source, 0, headerPackKlvSize, + 0L, 0L, newFooterOffset, headerNewHeaderByteCount); + fos.write(headerPackBytes); + if (headerPaddingSize > 0) { + copyRawBytes(srcChannel, destChannel, headerPackKlvSize, headerPaddingSize); } - - - RandomIndexItem hRipItem = new RandomIndexItemImpl(headerPartitionPack.getBodySID(), thisPartition); - ripItems.add(hRipItem); - - EssencePartition hContainerPartition = (EssencePartition)mxfFile.getPartitionAt(0); - if(hContainerPartition!=null) { - for ( EssenceElementImpl element = (EssenceElementImpl) hContainerPartition.readEssenceElement() ; - element != null ; - element = (EssenceElementImpl) hContainerPartition.readEssenceElement() ) { - - ByteBuffer bb = element.getData(); - byte[] elementByteArray = bb.array(); - - ByteArrayOutputStream elementBytes = new ByteArrayOutputStream(elementByteArray.length); - - MXFStream.writeEssenceElement(elementBytes, element.getEssenceTrackIdentifier(), bb.array()); - elementBytes.writeTo(outputStream); - totalBytes += elementBytes.size(); - - } + fos.write(headerMetadataRegion); + + int partitionCount = mxfFile.countPartitions(); + long headerContentStart = headerMetadataOffset + headerOldHeaderByteCount; + long headerContentEnd = (partitionCount > 2) + ? ((BodyPartition) mxfFile.getPartitionAt(1)).getPartitionPack().getThisPartition() + : oldFooterOffset; + long headerContentLength = headerContentEnd - headerContentStart; + if (headerContentLength < 0) { + throw new IOException("Unexpected negative header content length while relocating header partition."); } - - int essenceCount = 0; - for ( int x = 1 ; x < mxfFile.countPartitions()-1; x++ ) { - BodyPartition originalPartition = (BodyPartition)mxfFile.getPartitionAt(x); - BodyPartition partition = originalPartition.clone(); - if(partition!=null) { - - if (partition instanceof FooterPartitionImpl) continue; - if (partition instanceof HeaderPartitionImpl) continue; - previousPartition = thisPartition; - thisPartition = totalBytes; - - BodyPartitionPack opp = originalPartition.getPartitionPack().clone(); - BodyPartitionPack partitionPack = partition.getPartitionPack().clone(); - - if (partitionPack instanceof GenericStreamPartitionPackImpl) { - GenericStreamPartitionPackImpl gsp = (GenericStreamPartitionPackImpl) partitionPack; - - gsp.setThisPartition(thisPartition); - gsp.setPreviousPartition(previousPartition); - gsp.setFooterPartition(footerOffset); - RandomIndexItem ripItem = new RandomIndexItemImpl(gsp.getBodySID(), thisPartition); - ripItems.add(ripItem); - - int partitionSize = (int) gsp.getEncodedSize(); - partitionSize = partitionSize < 65536 ? 65546 : partitionSize; - - try(ByteArrayOutputStream partitionBytes = new ByteArrayOutputStream(partitionSize)){ + copyRawBytes(srcChannel, destChannel, headerContentStart, headerContentLength); - MXFStream.writePartitionPack(partitionBytes, gsp); - @SuppressWarnings("unused") - long bytesAdded = ReadGenericStream(opp.getThisPartition() + opp.getEncodedSize() + 20, partitionBytes); + // ---- Body partitions (raw copy, only offset fields are patched) ---- + for (int i = 1; i < partitionCount - 1; i++) { + BodyPartition originalPartition = (BodyPartition) mxfFile.getPartitionAt(i); + PartitionPack bppInfo = originalPartition.getPartitionPack(); - partitionBytes.writeTo(outputStream); - totalBytes += partitionBytes.size(); - } - } - else { - partitionPack.setThisPartition(thisPartition); - partitionPack.setPreviousPartition(previousPartition); - partitionPack.setFooterPartition(footerOffset); - RandomIndexItem ripItem = new RandomIndexItemImpl(partitionPack.getBodySID(), thisPartition); - ripItems.add(ripItem); - - int partitionSize = (int) partitionPack.getEncodedSize(); - partitionSize = partitionSize < 65536 ? 65546 : partitionSize; + long oldThisPartition = bppInfo.getThisPartition(); + long oldPreviousPartition = bppInfo.getPreviousPartition(); + long partitionPackKlvSize = bppInfo.getEncodedSize() + 20; - try(ByteArrayOutputStream partitionBytes = new ByteArrayOutputStream(partitionSize)){ - - MXFStream.writePartitionPack(partitionBytes, partitionPack); - - IndexTableSegment it = partition.readIndexTableSegment(); - if(it!=null) { - MXFStream.writeIndexTableSegment(partitionBytes, it); - } - partitionBytes.writeTo(outputStream); - totalBytes += partitionBytes.size(); - } - - } - - //sids.put(partitionPack.getPreviousPartition(), totalBytes); - - - EssencePartition containerPartition = (EssencePartition) originalPartition; - - if(containerPartition!=null) { - essenceCount = essenceCount + 1; - for ( EssenceElementImpl element = (EssenceElementImpl) containerPartition.readEssenceElement() ; - element != null ; - element = (EssenceElementImpl) containerPartition.readEssenceElement() ) { - - - ByteBuffer bb = element.getData(); - byte[] elementByteArray = bb.array(); - - ByteArrayOutputStream elementBytes = new ByteArrayOutputStream(elementByteArray.length); - - MXFStream.writeEssenceElement(elementBytes, element.getEssenceTrackIdentifier(), bb.array()); - elementBytes.writeTo(outputStream); - totalBytes += elementBytes.size(); - } - } - + byte[] bppBytes = patchPartitionPackOffsets(source, oldThisPartition, partitionPackKlvSize, + oldThisPartition + headerDelta, + oldPreviousPartition == 0 ? 0 : oldPreviousPartition + headerDelta, + newFooterOffset, null); + fos.write(bppBytes); + + long contentStart = oldThisPartition + partitionPackKlvSize; + long contentEnd = (i + 1 < partitionCount - 1) + ? ((BodyPartition) mxfFile.getPartitionAt(i + 1)).getPartitionPack().getThisPartition() + : oldFooterOffset; + long contentLength = contentEnd - contentStart; + if (contentLength < 0) { + throw new IOException("Unexpected negative partition content length while relocating body partition " + i + "."); } + copyRawBytes(srcChannel, destChannel, contentStart, contentLength); } - System.out.println("Essence Count: " + essenceCount); - FooterPartition footerPartition = mxfFile.getFooterPartition(); - if (footerPartition != null) { - HeaderMetadata footerMetadata = footerPartition.readHeaderMetadata(); - FooterPartitionPack footerPartitionPack = footerPartition.getPartitionPack(); - Preface footerPreface = null; - if(footerMetadata != null) { - footerPreface = footerMetadata.getPreface(); - if(updatedCore!=null && coreFrameworkFooter != null) { - ContentStorage contentStorage = footerPreface.getContentStorageObject(); - Set packages = contentStorage.getPackages(); - - for(tv.amwa.maj.model.Package p : packages) { - if(p instanceof MaterialPackage) { - for(Track t : p.getPackageTracks()) { - if(t instanceof StaticTrack) { - StaticTrack st = (StaticTrack)t; - Segment ts = st.getTrackSegment(); - if(ts instanceof Sequence) { - - Sequence seq = (Sequence) ts; - List components = seq.getComponentObjects(); - - for(Component c : components) { - if(c instanceof DescriptiveMarkerImpl) { - DescriptiveMarker m = (DescriptiveMarker) c; - try { - DescriptiveFramework df = m.getDescriptiveFrameworkObject(); - - if(df!=null && df instanceof AS07CoreDMSFramework) { - m.setDescriptiveFrameworkObject(updatedCore); - } - } - catch(PropertyNotPresentException pnp) { - LOGGER.log(Level.INFO, pnp.toString()); - } - } - } - } - } - } - } - } - } - } - - - previousPartition = thisPartition; - thisPartition = totalBytes; - - RandomIndexItem ripItem = new RandomIndexItemImpl(footerPartitionPack.getBodySID(), thisPartition); - ripItems.add(ripItem); - - footerPartitionPack.setThisPartition(thisPartition); - footerPartitionPack.setPreviousPartition(previousPartition); - footerPartitionPack.setFooterPartition(thisPartition); - - int footerSize = (int) footerPartitionPack.getEncodedSize(); - footerSize = footerSize < 65536 ? 65546 : footerSize; - try(ByteArrayOutputStream footerBytes = new ByteArrayOutputStream(footerSize)){ - try(ByteArrayOutputStream metadataBytes = new ByteArrayOutputStream(footerSize)){ - - if(footerPreface!=null){ - MXFStream.writeHeaderMetadata(metadataBytes, footerPreface); - footerPartitionPack.setHeaderByteCount(metadataBytes.size() + klvFillLength); - MXFStream.writeFill(metadataBytes, klvFillLength); - } + // ---- Footer partition ---- + long footerPackKlvSize = footerPackOriginal.getEncodedSize() + 20; + long footerOldHeaderByteCount = footerPackOriginal.getHeaderByteCount(); + long oldFooterPrevious = footerPackOriginal.getPreviousPartition(); + long footerPaddingSize = detectPartitionPackPadding(source, oldFooterOffset + footerPackKlvSize); + long footerMetadataOffset = oldFooterOffset + footerPackKlvSize + footerPaddingSize; - MXFStream.writePartitionPack(footerBytes, footerPartitionPack); + byte[] footerMetadataRegion; + long footerNewHeaderByteCount; + if (footerPlan != null) { + footerNewHeaderByteCount = footerPlan.newHeaderByteCount; + footerMetadataRegion = buildMetadataRegionBytes(footerPlan); + } else { + footerNewHeaderByteCount = footerOldHeaderByteCount; + footerMetadataRegion = readRawBytes(source, footerMetadataOffset, footerOldHeaderByteCount); + } - footerBytes.writeTo(outputStream); - metadataBytes.writeTo(outputStream); + byte[] footerPackBytes = patchPartitionPackOffsets(source, oldFooterOffset, footerPackKlvSize, + newFooterOffset, oldFooterPrevious == 0 ? 0 : oldFooterPrevious + headerDelta, + newFooterOffset, footerNewHeaderByteCount); + fos.write(footerPackBytes); + if (footerPaddingSize > 0) { + copyRawBytes(srcChannel, destChannel, oldFooterOffset + footerPackKlvSize, footerPaddingSize); + } + fos.write(footerMetadataRegion); + + long footerContentStart = footerMetadataOffset + footerOldHeaderByteCount; + long footerContentLength = oldRipStart - footerContentStart; + if (footerContentLength < 0) { + throw new IOException("Unexpected negative footer content length while relocating footer partition."); + } + copyRawBytes(srcChannel, destChannel, footerContentStart, footerContentLength); + + // ---- Random Index Pack ---- + // getPartitionIndex() returns defensive clones, so the shifted items must be + // written back explicitly via setPartitionIndex() rather than mutated in place. + if (rip != null) { + RandomIndexItem[] items = rip.getPartitionIndex(); + for (RandomIndexItem item : items) { + long offset = item.getByteOffset(); + if (offset != 0) { + item.setByteOffset(offset + headerDelta); } } - + rip.setPartitionIndex(items); + MXFStream.writeRandomIndexPack(fos, rip); } - - RandomIndexPackImpl ripOriginal = (RandomIndexPackImpl)mxfFile.getRandomIndexPack(); - System.out.println(ripOriginal); - System.out.println("********************************"); - ripOriginal.clear(); - - RandomIndexItem list2[] = new RandomIndexItem[ripItems.size()]; - ripOriginal.setPartitionIndex(ripItems.toArray(list2)); - System.out.println(ripOriginal); - if (ripOriginal != null) { - MXFStream.writeRandomIndexPack(outputStream, ripOriginal); - } - outputStream.close(); - - Path p = Paths.get(outputFilePath); - Path p2 = Paths.get(tempFilePath); - if(Files.exists(p)) Files.delete(p); - - Files.copy(p2, Paths.get(outputFilePath)); - - if(Files.exists(p2)) Files.delete(p2); - } catch(Exception ex) { - LOGGER.log(Level.WARNING, ex.toString(), ex); - Path p = Paths.get(tempFilePath); - if(Files.exists(p)) Files.delete(p); - result.setException(ex); - result.setSuccess(false); - return result; } - mxfFile.close(); + + Path finalPath = Paths.get(outputFilePath); + Path tempPath = Paths.get(tempFilePath); + if (Files.exists(finalPath)) Files.delete(finalPath); + Files.move(tempPath, finalPath); + + result.setSuccess(true); } catch (Exception ex) { LOGGER.log(Level.WARNING, ex.toString(), ex); + try { + Path tempPath = Paths.get(tempFilePath); + if (Files.exists(tempPath)) Files.delete(tempPath); + } catch (IOException ignore) { + } result.setException(ex); result.setSuccess(false); - return result; } - result.setSuccess(true); return result; } - - public long ReadGenericStream(long offset, OutputStream outputStream) { - // Read length - try (InputStream is = new FileInputStream(filePath)){ - MXFStream.skipForward(is, offset); - KeyAndConsumed key = MXFStream.readKey(is); - LengthAndConsumed length = MXFStream.readBERLength(is); - ByteBuffer bb = MXFStream.readValue(is, length.getLength()); - is.close(); - - MXFStream.writeKey(outputStream, key.getKey()); - MXFStream.writeBERLength(outputStream, length.getLength(), (int)length.getConsumed()); - MXFStream.writeValue(outputStream, bb); - - return key.getConsumed() + length.getConsumed() + length.getLength(); - - } catch (FileNotFoundException e) { - LOGGER.log(Level.WARNING, e.toString(), e); - } catch (IOException e) { - LOGGER.log(Level.WARNING, e.toString(), e); + + private static byte[] readRawBytes(RandomAccessFile source, long offset, long length) throws IOException { + byte[] buffer = new byte[(int) length]; + source.seek(offset); + source.readFully(buffer); + return buffer; + } + + private static final int PP_VALUE_START = 20; + private static final int PP_THIS_PARTITION_OFFSET = PP_VALUE_START + 8; + private static final int PP_PREVIOUS_PARTITION_OFFSET = PP_VALUE_START + 16; + private static final int PP_FOOTER_PARTITION_OFFSET = PP_VALUE_START + 24; + private static final int PP_HEADER_BYTE_COUNT_OFFSET = PP_VALUE_START + 32; + + private static byte[] patchPartitionPackOffsets(RandomAccessFile source, long packStart, long packKlvSize, + long newThisPartition, long newPreviousPartition, long newFooterPartition, Long newHeaderByteCount) throws IOException { + byte[] raw = readRawBytes(source, packStart, packKlvSize); + putUInt64BE(raw, PP_THIS_PARTITION_OFFSET, newThisPartition); + putUInt64BE(raw, PP_PREVIOUS_PARTITION_OFFSET, newPreviousPartition); + putUInt64BE(raw, PP_FOOTER_PARTITION_OFFSET, newFooterPartition); + if (newHeaderByteCount != null) { + putUInt64BE(raw, PP_HEADER_BYTE_COUNT_OFFSET, newHeaderByteCount); + } + return raw; + } + + private static void putUInt64BE(byte[] data, int offset, long value) { + for (int i = 7; i >= 0; i--) { + data[offset + i] = (byte) (value & 0xFF); + value >>>= 8; + } + } + + private static void copyRawBytes(FileChannel srcChannel, WritableByteChannel destChannel, long offset, long length) throws IOException { + long position = offset; + long remaining = length; + while (remaining > 0) { + long transferred = srcChannel.transferTo(position, remaining, destChannel); + if (transferred <= 0) { + throw new IOException("Failed to copy raw bytes from source file at offset " + position + "."); + } + position += transferred; + remaining -= transferred; } - return 0; } @Override public boolean DownloadGenericStream(int streamId, String outputFile) { @@ -742,7 +853,7 @@ public ByteBuffer GetGenericStream(int streamId) { BodyPartitionPack opp = originalPartition.getPartitionPack().clone(); if(opp.getBodySID()==streamId) { - ByteBuffer bb = GetGenericStream(opp.getThisPartition() + opp.getEncodedSize() + 20); + ByteBuffer bb = GetGenericStream(opp.getThisPartition() + opp.getEncodedSize() + 20 + opp.getHeaderByteCount() + opp.getIndexByteCount()); return bb; } } @@ -1074,6 +1185,21 @@ public AS07CoreDMSFramework getAS07CoreDMSFramework() { return coreFramework; } + + /** + * Computes the size of a KLV filler needed to align the next item to a KAG boundary. + * Returns 0 if already aligned. The minimum returned size is 20 bytes (the smallest + * KLV fill item MXFStream can write). + */ + private static long calculateKAGFillerSize(long currentSize, long kagSize) { + if (kagSize <= 0) return 0; + long remainder = currentSize % kagSize; + if (remainder == 0) return 0; + long filler = kagSize - remainder; + if (filler < 20) filler += kagSize; + return filler; + } + public void setAS07CoreDMSFramework(AS07CoreDMSFramework dms) { AS07CoreDMSFramework coreFrameworkHeader = getAS07CoreDMSFrameworkHeader(); AS07CoreDMSFramework coreFrameworkFooter = getAS07CoreDMSFrameworkFooter();