Locked History Actions

mp4parser/cantdeletesource

ソース動画を削除できなくなる(Can't delete source movie file)

例えば、ある動画を元に(複数でもよい)、別の動画を作成すると、作成終了してクローズしているにもかかわらず、ソース動画が削除できなくなる。 この理由は、mp4parser内のコピー操作中にFileChannelをmapしてByteBufferを得ているため。元のFileChannelをクローズしても、ByteBufferが存在する限り実際にはオープンしたまま。これを明示的に削除するオフィシャルな方法は存在せず、GCを待つしか無い。つまり、コピー直後にソースを削除するにはSystem.gc()を呼び出す必要がある。 あるいは、アプリを終了させるしかない。

You can't delete source movie file just after copying it to destination movie file. This happens because mp4parser uses FileChannel and getting DirectByteBuffer by map()ping source file. So usually you can't delete source movie file until Java System decides to garbage-collect all of those DirectByteBuffers. There's no official method for deleting them. So your only choice is calling System.gc() in a official way. (or of course exiting the application)。

Tested at mp4parser version 1.1.21

import java.io.*;
import java.nio.channels.*;

import com.coremedia.iso.boxes.*;
import com.googlecode.mp4parser.*;
import com.googlecode.mp4parser.authoring.*;
import com.googlecode.mp4parser.authoring.builder.*;
import com.googlecode.mp4parser.authoring.container.mp4.*;
import com.googlecode.mp4parser.authoring.tracks.*;

public class TestApp {

  static File testFolder = new File("some location");

  public static void main(String[]arg) throws Exception {
    File srcFile = new File(testFolder, "1.mp4");
    copy(srcFile);
    System.gc(); // REQUIRED 
    System.out.println("" + srcFile.delete());
  }
  
  static void copy(File srcFile) throws Exception {    
    
    FileDataSourceImpl srcData = new FileDataSourceImpl(srcFile);
    Movie srcMovie = MovieCreator.build(srcData);

    File dstFile = new File(testFolder, "output.mp4");
    dstFile.delete();
    Movie dstMovie = new Movie();

    for (Track track: srcMovie.getTracks()) dstMovie.addTrack(new AppendTrack(track));

    Container container = new DefaultMp4Builder().build(dstMovie);
    FileChannel fc = new RandomAccessFile(dstFile, "rw").getChannel();    
    container.writeContainer(fc);
    fc.close();

    srcData.close();
  }

}

もう一つとして、FileDataSourceImplを継承し、close()を書き換え、その中でアンオフィシャルな方法でクリーンアップをする方法がある。 SoftReferenceを使用する理由は、DefaultMp4SampleListの中でもSoftReferenceでキャッシュされているため。

Another way is creating sub-class of FileDataSourceImpl overrding close() method. In there you can use unofficial way of clean up DirectByteBuffer. The reason I used SoftReference is because there's SoftReferences of DirectByteBuffer in DefaultMp4SampleList of mp4parser.

import java.io.*;
import java.lang.ref.*;
import java.lang.reflect.*;
import java.nio.*;
import java.util.*;

import com.googlecode.mp4parser.*;

public class CloseableFileDataSourceImpl extends FileDataSourceImpl {
  
  private static Method sunCleaner;
  
  static {
    try {
      sunCleaner = Class.forName("sun.misc.Cleaner").getMethod("clean");
      sunCleaner.setAccessible(true);    
    } catch (Exception ex) {
      sunCleaner = null;
    }
  }
  
  private Slot slots;
  
  static class Slot {
    Slot prev;
    SoftReference<ByteBuffer>sbb;
    Slot(Slot prev, ByteBuffer bb) {
      this.prev = prev;
      this.sbb = new SoftReference<>(bb);
    }
  }
  
  public CloseableFileDataSourceImpl(File srcFile) throws FileNotFoundException {
    super(srcFile);
  }

  @Override
  public synchronized ByteBuffer map(long startPosition, long size) throws IOException {
    ByteBuffer bb = super.map(startPosition, size);
    if (bb.isDirect() && sunCleaner != null) {
      slots = new Slot(slots, bb);
    }
    return bb;
  }
  
  @Override
  public void close() throws IOException {
    while (slots != null) {
      closeDirectBuffer(slots.sbb.get());
      slots = slots.prev;
    }
    super.close();
  }
  
  private static Map<Class<?>, Method>methods = new HashMap<Class<?>, Method>();
  
  private static synchronized void closeDirectBuffer(ByteBuffer bb) {
    if (bb == null) return;
    
    // getting direct cleaner
    Method directCleaner = methods.get(bb.getClass());
    if (directCleaner == null) {
      try {
        directCleaner = bb.getClass().getMethod("cleaner");
        directCleaner.setAccessible(true);
      } catch (Exception ex) {      
        return;
      }
      methods.put(bb.getClass(), directCleaner);
    }

    // do clean
    try {
      sunCleaner.invoke(directCleaner.invoke(bb));
    } catch(Exception ex) { }
    bb = null;
  }
}

参考(references)