Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #645 SeekableOutputStream #1350

Merged
merged 5 commits into from
Dec 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

* Add `Seekable` and `SeekableByteArrayOutputStream` to be used with `FFmpegFrameRecorder` ([pull #1350](https://github.com/bytedeco/javacv/pull/1350))
* Update `RealSense2FrameGrabber` with support for sensor options and fix for multiple devices ([pull #1348](https://github.com/bytedeco/javacv/pull/1348))

### November 5, 2019 version 1.5.2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
* Copyright (C) 2019 Sven Vorlauf
*
* Licensed either under the Apache License, Version 2.0, or (at your option)
* under the terms of the GNU General Public License as published by
* the Free Software Foundation (subject to the "Classpath" exception),
* either version 2, or any later version (collectively, 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
* http://www.gnu.org/licenses/
* http://www.gnu.org/software/classpath/license.html
*
* or as provided in the LICENSE.txt file that accompanied this code.
* 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.bytedeco.javacv;

import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Random;

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacpp.indexer.UByteIndexer;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.Test;

public class SeekableByteArrayOutputStreamTest {

private static final int WIDTH = 640;
private static final int HEIGHT = 360;
private static final int FRAME_COUNT = 100;

private int writeByte(byte[] originalBytes, int offset, SeekableByteArrayOutputStream byteArrayOutputStream) {
byteArrayOutputStream.write(originalBytes[offset]);
return 1;
}

private int writePartialBytes(byte[] originalBytes, int offset, Random random,
SeekableByteArrayOutputStream byteArrayOutputStream) throws IOException {
int chunkSize = Math.min(random.nextInt(50), originalBytes.length - offset);
byteArrayOutputStream.write(originalBytes, offset, chunkSize);
return chunkSize;
}

private int writeBytes(byte[] originalBytes, int offset, Random random,
SeekableByteArrayOutputStream byteArrayOutputStream) throws IOException {
int chunkSize = Math.min(random.nextInt(50), originalBytes.length - offset);
byteArrayOutputStream.write(Arrays.copyOfRange(originalBytes, offset, offset + chunkSize));
return chunkSize;
}

private void createVideo(FFmpegFrameRecorder recorder) throws Exception {
recorder.setVideoCodec(avcodec.AV_CODEC_ID_MPEG4);
recorder.setFormat("mp4");
recorder.setFrameRate(30);
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.start();
for (int n = 0; n < FRAME_COUNT; n++) {
Frame frame = new Frame(WIDTH, HEIGHT, Frame.DEPTH_UBYTE, 3);
UByteIndexer frameIdx = frame.createIndexer();
for (int i = 0; i < frameIdx.rows(); i++) {
for (int j = 0; j < frameIdx.cols(); j++) {
for (int k = 0; k < frameIdx.channels(); k++) {
frameIdx.put(i, j, k, n + i + j + k);
}
}
}
recorder.record(frame);
}
recorder.close();
}

@Test
public void serialWriteByteTest() {
System.out.println("SeekableByteArrayOutputStreamSerialWriteByte");
Random random = new Random(-1);
byte[] originalBytes = new byte[1000];
random.nextBytes(originalBytes);

try (SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream()) {
int offset = 0;
while (offset < originalBytes.length) {
offset += writeByte(originalBytes, offset, byteArrayOutputStream);
}
assertArrayEquals(originalBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
}
}

@Test
public void serialWriteBytesTest() {
System.out.println("SeekableByteArrayOutputStreamSerialWriteBytes");
Random random = new Random(-1);
byte[] originalBytes = new byte[1000];
random.nextBytes(originalBytes);

try (SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream()) {
int offset = 0;
while (offset < originalBytes.length) {
offset += writeBytes(originalBytes, offset, random, byteArrayOutputStream);
}
assertArrayEquals(originalBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
}
}

@Test
public void serialWritePartialBytesTest() {
System.out.println("SeekableByteArrayOutputStreamSerialWritePartialBytes");
Random random = new Random(-1);
byte[] originalBytes = new byte[1000];
random.nextBytes(originalBytes);

try (SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream()) {
int offset = 0;
while (offset < originalBytes.length) {
offset += writePartialBytes(originalBytes, offset, random, byteArrayOutputStream);
}
assertArrayEquals(originalBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
}
}

@Test
public void serialWriteTest() {
System.out.println("SeekableByteArrayOutputStreamSerialWrite");
Random random = new Random(-1);
byte[] originalBytes = new byte[1000];
random.nextBytes(originalBytes);

try (SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream()) {
int offset = 0;
while (offset < originalBytes.length) {
switch (random.nextInt(3)) {
case 0:
offset += writeByte(originalBytes, offset, byteArrayOutputStream);
break;
case 1:
offset += writeBytes(originalBytes, offset, random, byteArrayOutputStream);
break;
case 2:
offset += writePartialBytes(originalBytes, offset, random, byteArrayOutputStream);
break;
}
}
assertArrayEquals(originalBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
}
}

public void seekWriteTest() {
System.out.println("SeekableByteArrayOutputStreamSeekWrite");
Random random = new Random(-1);
byte[] originalBytes = new byte[1000];
random.nextBytes(originalBytes);
try (SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream()) {
int offset = 0;
for (int i = 0; i < 10; i++) {
// write 100 bytes
byteArrayOutputStream.write(originalBytes, offset, 100);

int position = random.nextInt(offset + 20);
int newBytesPosition = position + 500 % 1000;
int length = 10 + random.nextInt(20);
// get current bytes
byte[] writtenOriginalBytes = Arrays.copyOfRange(byteArrayOutputStream.toByteArray(), position,
position + length);

// bytes to write at the new position
byte[] newBytes = Arrays.copyOfRange(originalBytes, newBytesPosition, newBytesPosition + length);

// just assert that the new bytes are different to the written ones
assertThat(writtenOriginalBytes, IsNot.not(IsEqual.equalTo(newBytes)));

// replace bytes
byteArrayOutputStream.seek(position, 0);
byteArrayOutputStream.write(newBytes);
byte[] writtenNewBytes = Arrays.copyOfRange(byteArrayOutputStream.toByteArray(), position,
position + length);
assertThat(newBytes, IsEqual.equalTo(writtenNewBytes));

// write back original bytes
byteArrayOutputStream.seek(position, 0);
byteArrayOutputStream.write(originalBytes, position, length);

// get back to the end of the stream
byteArrayOutputStream.seek(offset, 0);
offset += 100;
}
while (offset < originalBytes.length) {
switch (random.nextInt(3)) {
case 0:
offset += writeByte(originalBytes, offset, byteArrayOutputStream);
break;
case 1:
offset += writeBytes(originalBytes, offset, random, byteArrayOutputStream);
break;
case 2:
offset += writePartialBytes(originalBytes, offset, random, byteArrayOutputStream);
break;
}
}
assertArrayEquals(originalBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
}
}

@Test
public void testVideoBytesEqual() {
// if this test fails it might be due to indeterministic multithreaded encoding
System.out.println("SeekableByteArrayOutputStreamVideo");
File tempFile = new File(Loader.getTempDir(), "test.mp4");
try {
createVideo(new FFmpegFrameRecorder(tempFile, WIDTH, HEIGHT, 0));
byte[] fileBytes = Files.readAllBytes(tempFile.toPath());

SeekableByteArrayOutputStream byteArrayOutputStream = new SeekableByteArrayOutputStream();
createVideo(new FFmpegFrameRecorder(byteArrayOutputStream, WIDTH, HEIGHT, 0));
assertArrayEquals(fileBytes, byteArrayOutputStream.toByteArray());
} catch (Exception e) {
fail("Exception should not have been thrown: " + e);
} finally {
tempFile.delete();
}
}
}
23 changes: 20 additions & 3 deletions src/main/java/org/bytedeco/javacv/FFmpegFrameRecorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public void releaseUnsafe() throws Exception {

/* free the streams */
int nb_streams = oc.nb_streams();
for(int i = 0; i < nb_streams; i++) {
for (int i = 0; i < nb_streams; i++) {
av_free(oc.streams(i).codec());
av_free(oc.streams(i));
}
Expand Down Expand Up @@ -322,6 +322,23 @@ static class WriteCallback extends Write_packet_Pointer_BytePointer_int {

static WriteCallback writeCallback = new WriteCallback().retainReference();

static class SeekCallback extends Seek_Pointer_long_int {

@Override public long call(Pointer opaque, long offset, int whence) {
try {
OutputStream os = outputStreams.get(opaque);
((Seekable)os).seek(offset, whence);
return 0;
}
catch (Throwable t) {
System.err.println("Error on OutputStream.seek(): " + t);
return -1;
}
}
}

static SeekCallback seekCallback = new SeekCallback().retainReference();

private OutputStream outputStream;
private boolean closeOutputStream;
private AVIOContext avio;
Expand Down Expand Up @@ -424,7 +441,7 @@ public void startUnsafe() throws Exception {
}

if (outputStream != null) {
avio = avio_alloc_context(new BytePointer(av_malloc(4096)), 4096, 1, oc, null, writeCallback, null);
avio = avio_alloc_context(new BytePointer(av_malloc(4096)), 4096, 1, oc, null, writeCallback, seekCallback);
oc.pb(avio);

filename = outputStream.toString();
Expand Down Expand Up @@ -634,7 +651,7 @@ public void startUnsafe() throws Exception {
throw new Exception("avcodec_alloc_context3() error: Could not allocate audio encoding context.");
}

if(inpAudioStream != null && audioChannels > 0){
if (inpAudioStream != null && audioChannels > 0) {
if ((ret = avcodec_copy_context(audio_st.codec(), inpAudioStream.codec())) < 0) {
throw new Exception("avcodec_copy_context() error:\tFailed to copy context from input audio to output audio stream codec context\n");
}
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/org/bytedeco/javacv/Seekable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2019 Sven Vorlauf
*
* Licensed either under the Apache License, Version 2.0, or (at your option)
* under the terms of the GNU General Public License as published by
* the Free Software Foundation (subject to the "Classpath" exception),
* either version 2, or any later version (collectively, 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
* http://www.gnu.org/licenses/
* http://www.gnu.org/software/classpath/license.html
*
* or as provided in the LICENSE.txt file that accompanied this code.
* 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.bytedeco.javacv;

public interface Seekable {

public void seek(long offset, int whence);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2019 Sven Vorlauf
*
* Licensed either under the Apache License, Version 2.0, or (at your option)
* under the terms of the GNU General Public License as published by
* the Free Software Foundation (subject to the "Classpath" exception),
* either version 2, or any later version (collectively, 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
* http://www.gnu.org/licenses/
* http://www.gnu.org/software/classpath/license.html
*
* or as provided in the LICENSE.txt file that accompanied this code.
* 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.bytedeco.javacv;

import java.io.ByteArrayOutputStream;

public class SeekableByteArrayOutputStream extends ByteArrayOutputStream implements Seekable {

long position;

@Override public void seek(long position, int whence) {
if (position < 0 || position > count || whence != 0)
throw new IllegalArgumentException();
this.position = position;
}

@Override public synchronized void write(int b) {
if (position < count) {
buf[(int) position] = (byte) b; // position < count <= MAX_INT
} else {
super.write(b);
}
position++;
}

@Override public synchronized void write(byte[] b, int off, int len) {
if (position < count) {
for (int i = 0 ; i < len ; i++) {
write(b[off + i]); // should be changed for bigegr arrays
}
} else {
super.write(b, off, len);
position = count;
}
}
}