org.red5.io.flv.impl.FLVWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ant-media-server Show documentation
Show all versions of ant-media-server Show documentation
Ant Media Server supports RTMP, RTSP, MP4, HLS, WebRTC, Adaptive Streaming, etc.
/*
* RED5 Open Source Media Server - https://github.com/Red5/ Copyright 2006-2016 by respective authors (see below). All rights reserved. Licensed under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless
* required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
package org.red5.io.flv.impl;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.codec.AudioCodec;
import org.red5.codec.VideoCodec;
import org.red5.io.IStreamableFile;
import org.red5.io.ITag;
import org.red5.io.ITagWriter;
import org.red5.io.amf.Input;
import org.red5.io.amf.Output;
import org.red5.io.flv.FLVHeader;
import org.red5.io.flv.IFLV;
import org.red5.io.object.Deserializer;
import org.red5.io.utils.IOUtils;
import org.red5.media.processor.IPostProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Writer is used to write the contents of a FLV file
*
* @author The Red5 Project
* @author Dominick Accattato ([email protected])
* @author Luke Hubbard, Codegent Ltd ([email protected])
* @author Tiago Jacobs ([email protected])
* @author Paul Gregoire ([email protected])
*/
public class FLVWriter implements ITagWriter {
private static Logger log = LoggerFactory.getLogger(FLVWriter.class);
/**
* Length of the flv header in bytes
*/
private final static int HEADER_LENGTH = 9;
/**
* Length of the flv tag in bytes
*/
private final static int TAG_HEADER_LENGTH = 11;
/**
* For now all recorded streams carry a stream id of 0.
*/
private final static byte[] DEFAULT_STREAM_ID = new byte[] { (byte) (0 & 0xff), (byte) (0 & 0xff), (byte) (0 & 0xff) };
/**
* Executor for tasks within this instance
*/
private ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* FLV object
*/
private static IFLV flv;
/**
* Number of bytes written
*/
private AtomicLong bytesWritten = new AtomicLong(0);
/**
* Position in file
*/
private int offset;
/**
* Position in file
*/
private int timeOffset;
/**
* Id of the audio codec used.
*/
private volatile int audioCodecId = -1;
/**
* Id of the video codec used.
*/
private volatile int videoCodecId = -1;
/**
* If audio configuration data has been written
*/
private AtomicBoolean audioConfigWritten = new AtomicBoolean(false);
/**
* If video configuration data has been written
*/
private AtomicBoolean videoConfigWritten = new AtomicBoolean(false);
/**
* Sampling rate
*/
private volatile int soundRate;
/**
* Size of each audio sample
*/
private volatile int soundSize;
/**
* Mono (0) or stereo (1) sound
*/
private volatile boolean soundType;
/**
* Are we appending to an existing file?
*/
private boolean append;
/**
* Duration of the file.
*/
private int duration;
/**
* Size of video data
*/
private int videoDataSize = 0;
/**
* Size of audio data
*/
private int audioDataSize = 0;
/**
* Flv output destination.
*/
private SeekableByteChannel fileChannel;
/**
* Destination to which stream data is stored without an flv header.
*/
private SeekableByteChannel dataChannel;
// path to the original file passed to the writer
private String filePath;
private final Semaphore lock = new Semaphore(1, true);
// the size of the last tag written, which includes the tag header length
private volatile int lastTagSize;
// to be executed after flv is finalized
private LinkedList postProcessors;
// state of flv finalization
private AtomicBoolean finalized = new AtomicBoolean(false);
// iso 8601 date of recording
private String recordedDate = ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT);
// metadata
private Map meta;
// offset in previous flv to skip when appending
private long appendOffset = HEADER_LENGTH + 4L;
/**
* Creates writer implementation with for a given file
*
* @param filePath
* path to existing file
*/
public FLVWriter(String filePath) {
this.filePath = filePath;
log.debug("Writing to: {}", filePath);
try {
createDataFile();
} catch (Exception e) {
log.error("Failed to create FLV writer", e);
}
}
/**
* Creates writer implementation with for a given file
*
* @param repair
* repair the .ser file
*
* @param filePath
* path to existing file
*/
public FLVWriter(boolean repair, String filePath) {
this.filePath = filePath;
try {
createRepairDataFile();
} catch (Exception e) {
log.error("Failed to create FLV writer", e);
}
}
/**
* Creates writer implementation with given file and flag indicating whether or not to append.
*
* FLV.java uses this constructor so we have access to the file object
*
* @param file
* File output stream
* @param append
* true if append to existing file
*/
public FLVWriter(File file, boolean append) {
this(file.toPath(), append);
}
/**
* Creates writer implementation with given file and flag indicating whether or not to append.
*
* FLV.java uses this constructor so we have access to the file object
*
* @param path
* File output path
* @param append
* true if append to existing file
*/
public FLVWriter(Path path, boolean append) {
filePath = path.toFile().getAbsolutePath();
this.append = append;
log.debug("Writing to: {} {}", filePath, flv);
try {
if (append) {
// get previous metadata
meta = getMetaData(path, 5);
if (meta != null) {
for (Entry entry : meta.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if ("duration".equals(key)) {
if (value instanceof Double) {
Double d = (((Double) value) * 1000d);
duration = d.intValue();
} else {
duration = Integer.valueOf((String) value) * 1000;
}
} else if ("recordeddate".equals(key)) {
recordedDate = String.valueOf(value);
}
}
// if we are appending get the duration as offset
timeOffset = duration;
log.debug("Duration: {}", duration);
}
// move / rename previous flv
Files.move(path, path.resolveSibling(path.toFile().getName().replace(".flv", ".old")));
log.debug("Previous flv renamed");
}
createDataFile();
} catch (Exception e) {
log.error("Failed to create FLV writer", e);
}
}
private Map getMetaData(Path path, int maxTags) throws IOException {
Map meta = null;
// attempt to read the metadata
try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
long size = channel.size();
log.debug("Channel open: {} size: {} position: {}", channel.isOpen(), size, channel.position());
if (size > 0L) {
// skip flv signature 4b, flags 1b, data offset 4b (9b), prev tag size (4b)
channel.position(appendOffset);
// flv tag header size 11b
ByteBuffer dst = ByteBuffer.allocate(11);
do {
int read = channel.read(dst);
if (read > 0) {
dst.flip();
byte tagType = (byte) (dst.get() & 31); // 1
int bodySize = IOUtils.readUnsignedMediumInt(dst); // 3
int timestamp = IOUtils.readExtendedMediumInt(dst); // 4
int streamId = IOUtils.readUnsignedMediumInt(dst); // 3
log.debug("Data type: {} timestamp: {} stream id: {} body size: {}", new Object[] { tagType, timestamp, streamId, bodySize });
if (tagType == ITag.TYPE_METADATA) {
ByteBuffer buf = ByteBuffer.allocate(bodySize);
read = channel.read(buf);
if (read > 0) {
buf.flip();
// construct the meta
IoBuffer ioBuf = IoBuffer.wrap(buf);
Input input = new Input(ioBuf);
String metaType = Deserializer.deserialize(input, String.class);
log.debug("Metadata type: {}", metaType);
meta = Deserializer.deserialize(input, Map.class);
input = null;
ioBuf.clear();
ioBuf.free();
if (meta.containsKey("duration")) {
appendOffset = channel.position() + 4L;
break;
}
}
buf.compact();
}
// advance beyond prev tag size
channel.position(channel.position() + 4L);
//int prevTagSize = dst.getInt(); // 4
//log.debug("Previous tag size: {} {}", prevTagSize, (bodySize - 11));
dst.compact();
}
} while (--maxTags > 0); // read up-to "max" tags looking for duration
}
}
return meta;
}
/**
* Writes the header bytes
*
* @throws IOException
* Any I/O exception
*/
@Override
public void writeHeader() throws IOException {
// create a buffer
ByteBuffer buf = ByteBuffer.allocate(HEADER_LENGTH + 4); // FLVHeader (9 bytes) + PreviousTagSize0 (4 bytes)
// instance an flv header
FLVHeader flvHeader = new FLVHeader();
flvHeader.setFlagAudio(audioCodecId != -1 ? true : false);
flvHeader.setFlagVideo(videoCodecId != -1 ? true : false);
// write the flv header in the buffer
flvHeader.write(buf);
// the final version of the file will go here
createOutputFile();
// write header to output channel
bytesWritten.set(fileChannel.write(buf));
assert ((HEADER_LENGTH + 4) - bytesWritten.get() == 0);
log.debug("Header size: {} bytes written: {}", (HEADER_LENGTH + 4), bytesWritten.get());
buf.clear();
buf = null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean writeTag(ITag tag) throws IOException {
// a/v config written flags
boolean onWrittenSetVideoFlag = false, onWrittenSetAudioFlag = false;
try {
lock.acquire();
/*
* Tag header = 11 bytes |-|---|----|---| 0 = type 1-3 = data size 4-7 = timestamp 8-10 = stream id (always 0) Tag data = variable bytes Previous tag = 4 bytes (tag header size +
* tag data size)
*/
log.trace("writeTag: {}", tag);
long prevBytesWritten = bytesWritten.get();
log.trace("Previous bytes written: {}", prevBytesWritten);
// skip tags with no data
int bodySize = tag.getBodySize();
log.trace("Tag body size: {}", bodySize);
// verify previous tag size stored in incoming tag
int previousTagSize = tag.getPreviousTagSize();
if (previousTagSize != lastTagSize) {
// use the last tag size
log.trace("Incoming previous tag size: {} does not match current value for last tag size: {}", previousTagSize, lastTagSize);
}
// ensure that the channel is still open
if (dataChannel != null) {
if (log.isTraceEnabled()) {
log.trace("Current file position: {}", dataChannel.position());
}
// get the data type
byte dataType = tag.getDataType();
// when tag is ImmutableTag which is in red5-server-common.jar, tag.getBody().reset() will throw InvalidMarkException because
// ImmutableTag.getBody() returns a new IoBuffer instance everytime.
IoBuffer tagBody = tag.getBody();
// set a var holding the entire tag size including the previous tag length
int totalTagSize = TAG_HEADER_LENGTH + bodySize + 4;
// create a buffer for this tag
ByteBuffer tagBuffer = ByteBuffer.allocate(totalTagSize);
// get the timestamp
int timestamp = tag.getTimestamp() + timeOffset;
// allow for empty tag bodies
byte[] bodyBuf = null;
if (bodySize > 0) {
// create an array big enough
bodyBuf = new byte[bodySize];
// put the bytes into the array
tagBody.get(bodyBuf);
// get the audio or video codec identifier
if (dataType == ITag.TYPE_AUDIO) {
audioDataSize += bodySize;
if (audioCodecId == -1) {
int id = bodyBuf[0] & 0xff; // must be unsigned
audioCodecId = (id & ITag.MASK_SOUND_FORMAT) >> 4;
log.debug("Audio codec id: {}", audioCodecId);
// if aac use defaults
if (audioCodecId == AudioCodec.AAC.getId()) {
log.trace("AAC audio type");
// Flash Player ignores these values and extracts the channel and sample rate data encoded in the AAC bit stream
soundRate = 44100;
soundSize = 16;
soundType = true;
// this is aac data, so a config chunk should be written before any media data
if (bodyBuf[1] == 0) {
// when this config is written set the flag
onWrittenSetAudioFlag = true;
} else {
// reject packet since config hasnt been written yet
log.debug("Rejecting AAC data since config has not yet been written");
return false;
}
} else if (audioCodecId == AudioCodec.SPEEX.getId()) {
log.trace("Speex audio type");
soundRate = 5500; // actually 16kHz
soundSize = 16;
soundType = false; // mono
} else {
switch ((id & ITag.MASK_SOUND_RATE) >> 2) {
case ITag.FLAG_RATE_5_5_KHZ:
soundRate = 5500;
break;
case ITag.FLAG_RATE_11_KHZ:
soundRate = 11000;
break;
case ITag.FLAG_RATE_22_KHZ:
soundRate = 22000;
break;
case ITag.FLAG_RATE_44_KHZ:
soundRate = 44100;
break;
}
log.debug("Sound rate: {}", soundRate);
switch ((id & ITag.MASK_SOUND_SIZE) >> 1) {
case ITag.FLAG_SIZE_8_BIT:
soundSize = 8;
break;
case ITag.FLAG_SIZE_16_BIT:
soundSize = 16;
break;
}
log.debug("Sound size: {}", soundSize);
// mono == 0 // stereo == 1
soundType = (id & ITag.MASK_SOUND_TYPE) > 0;
log.debug("Sound type: {}", soundType);
}
} else if (!audioConfigWritten.get() && audioCodecId == AudioCodec.AAC.getId()) {
// this is aac data, so a config chunk should be written before any media data
if (bodyBuf[1] == 0) {
// when this config is written set the flag
onWrittenSetAudioFlag = true;
} else {
// reject packet since config hasnt been written yet
return false;
}
}
} else if (dataType == ITag.TYPE_VIDEO) {
videoDataSize += bodySize;
if (videoCodecId == -1) {
int id = bodyBuf[0] & 0xff; // must be unsigned
videoCodecId = id & ITag.MASK_VIDEO_CODEC;
log.debug("Video codec id: {}", videoCodecId);
if (videoCodecId == VideoCodec.AVC.getId()) {
// this is avc/h264 data, so a config chunk should be written before any media data
if (bodyBuf[1] == 0) {
// when this config is written set the flag
onWrittenSetVideoFlag = true;
} else {
// reject packet since config hasnt been written yet
log.debug("Rejecting AVC data since config has not yet been written");
return false;
}
}
} else if (!videoConfigWritten.get() && videoCodecId == VideoCodec.AVC.getId()) {
// this is avc/h264 data, so a config chunk should be written before any media data
if (bodyBuf[1] == 0) {
// when this config is written set the flag
onWrittenSetVideoFlag = true;
} else {
// reject packet since config hasnt been written yet
log.debug("Rejecting AVC data since config has not yet been written");
return false;
}
}
}
}
// Data Type
IOUtils.writeUnsignedByte(tagBuffer, dataType); //1
// Body Size - Length of the message. Number of bytes after StreamID to end of tag
// (Equal to length of the tag - 11)
IOUtils.writeMediumInt(tagBuffer, bodySize); //3
// Timestamp
IOUtils.writeExtendedMediumInt(tagBuffer, timestamp); //4
// Stream id
tagBuffer.put(DEFAULT_STREAM_ID); //3
// get the body if we have one
if (bodyBuf != null) {
tagBuffer.put(bodyBuf);
}
// store new previous tag size
lastTagSize = TAG_HEADER_LENGTH + bodySize;
// we add the tag size
tagBuffer.putInt(lastTagSize);
// flip so we can process from the beginning
tagBuffer.flip();
// write the tag
dataChannel.write(tagBuffer);
bytesWritten.set(dataChannel.position());
if (log.isTraceEnabled()) {
log.trace("Tag written, check value: {} (should be 0)", (bytesWritten.get() - prevBytesWritten) - totalTagSize);
}
tagBuffer.clear();
// update the duration
log.debug("Current duration: {} timestamp: {}", duration, timestamp);
duration = Math.max(duration, timestamp);
// validate written amount
if ((bytesWritten.get() - prevBytesWritten) != totalTagSize) {
log.debug("Not all of the bytes appear to have been written, prev-current: {}", (bytesWritten.get() - prevBytesWritten));
}
return true;
} else {
// throw an exception and let them know the cause
throw new IOException("FLV write channel has been closed", new ClosedChannelException());
}
} catch (InterruptedException e) {
log.warn("Exception acquiring lock", e);
Thread.currentThread().interrupt();
} finally {
// update the file information
updateInfoFile();
// mark config written flags
if (onWrittenSetAudioFlag && audioConfigWritten.compareAndSet(false, true)) {
log.trace("Audio configuration written");
} else if (onWrittenSetVideoFlag && videoConfigWritten.compareAndSet(false, true)) {
log.trace("Video configuration written");
}
// release lock
lock.release();
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean writeTag(byte dataType, IoBuffer data) throws IOException {
if (timeOffset == 0) {
timeOffset = (int) System.currentTimeMillis();
}
try {
lock.acquire();
/*
* Tag header = 11 bytes |-|---|----|---| 0 = type 1-3 = data size 4-7 = timestamp 8-10 = stream id (always 0) Tag data = variable bytes Previous tag = 4 bytes (tag header size +
* tag data size)
*/
if (log.isTraceEnabled()) {
log.trace("writeTag - type: {} data: {}", dataType, data);
}
long prevBytesWritten = bytesWritten.get();
log.trace("Previous bytes written: {}", prevBytesWritten);
// skip tags with no data
int bodySize = data.limit();
log.debug("Tag body size: {}", bodySize);
// ensure that the channel is still open
if (dataChannel != null) {
log.debug("Current file position: {}", dataChannel.position());
// set a var holding the entire tag size including the previous tag length
int totalTagSize = TAG_HEADER_LENGTH + bodySize + 4;
// create a buffer for this tag
ByteBuffer tagBuffer = ByteBuffer.allocate(totalTagSize);
// Data Type
IOUtils.writeUnsignedByte(tagBuffer, dataType); //1
// Body Size - Length of the message. Number of bytes after StreamID to end of tag
// (Equal to length of the tag - 11)
IOUtils.writeMediumInt(tagBuffer, bodySize); //3
// Timestamp
int timestamp = (int) (System.currentTimeMillis() - timeOffset);
IOUtils.writeExtendedMediumInt(tagBuffer, timestamp); //4
// Stream id
tagBuffer.put(DEFAULT_STREAM_ID); //3
log.trace("Tag buffer (after tag header) limit: {} remaining: {}", tagBuffer.limit(), tagBuffer.remaining());
// get the body if we have one
if (data.hasArray()) {
tagBuffer.put(data.array());
log.trace("Tag buffer (after body) limit: {} remaining: {}", tagBuffer.limit(), tagBuffer.remaining());
}
// store new previous tag size
lastTagSize = TAG_HEADER_LENGTH + bodySize;
// we add the tag size
tagBuffer.putInt(lastTagSize);
log.trace("Tag buffer (after prev tag size) limit: {} remaining: {}", tagBuffer.limit(), tagBuffer.remaining());
// flip so we can process from the beginning
tagBuffer.flip();
if (log.isDebugEnabled()) {
//StringBuilder sb = new StringBuilder();
//HexDump.dumpHex(sb, tagBuffer.array());
//log.debug("\n{}", sb);
}
// write the tag
dataChannel.write(tagBuffer);
bytesWritten.set(dataChannel.position());
if (log.isTraceEnabled()) {
log.trace("Tag written, check value: {} (should be 0)", (bytesWritten.get() - prevBytesWritten) - totalTagSize);
}
tagBuffer.clear();
// update the duration
duration = Math.max(duration, timestamp);
log.debug("Writer duration: {}", duration);
// validate written amount
if ((bytesWritten.get() - prevBytesWritten) != totalTagSize) {
log.debug("Not all of the bytes appear to have been written, prev-current: {}", (bytesWritten.get() - prevBytesWritten));
}
return true;
} else {
// throw an exception and let them know the cause
throw new IOException("FLV write channel has been closed", new ClosedChannelException());
}
} catch (InterruptedException e) {
log.warn("Exception acquiring lock", e);
Thread.currentThread().interrupt();
} finally {
// update the file information
updateInfoFile();
// release lock
lock.release();
}
return false;
}
/** {@inheritDoc} */
@Override
public boolean writeStream(byte[] b) {
try {
dataChannel.write(ByteBuffer.wrap(b));
return true;
} catch (IOException e) {
log.error("", e);
}
return false;
}
/**
* Create the stream output file; the flv itself.
*
* @throws IOException
*/
private void createOutputFile() throws IOException {
this.fileChannel = Files.newByteChannel(Paths.get(filePath), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
}
/**
* Create the stream data file.
*
* @throws IOException
*/
private void createDataFile() throws IOException {
// temporary data file for storage of stream data
Path path = Paths.get(filePath + ".ser");
if (Files.deleteIfExists(path)) {
log.debug("Previous flv data file existed and was removed");
}
this.dataChannel = Files.newByteChannel(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.READ);
}
/**
* Create the stream data file for repair.
*
* @throws IOException
*/
private void createRepairDataFile() throws IOException {
// temporary data file for storage of stream data
Path path = Paths.get(filePath + ".ser");
// Create a data channel that is read-only
this.dataChannel = Files.newByteChannel(path, StandardOpenOption.READ);
}
/**
* Write "onMetaData" tag to the file.
*
* @param duration
* Duration to write in milliseconds.
* @param videoCodecId
* Id of the video codec used while recording.
* @param audioCodecId
* Id of the audio codec used while recording.
* @throws IOException
* if the tag could not be written
* @throws ExecutionException
* @throws InterruptedException
*/
private void writeMetadataTag(double duration, int videoCodecId, int audioCodecId) throws IOException, InterruptedException, ExecutionException {
log.debug("writeMetadataTag - duration: {} video codec: {} audio codec: {}", new Object[] { duration, videoCodecId, audioCodecId });
IoBuffer buf = IoBuffer.allocate(256);
buf.setAutoExpand(true);
Output out = new Output(buf);
out.writeString("onMetaData");
Map