Skip to content

Commit 706308e

Browse files
committed
progressive and interlaced scanning, and fixed video audio sync with previous frame
1 parent 81056e2 commit 706308e

4 files changed

Lines changed: 105 additions & 15 deletions

File tree

src/main/java/me/dynmie/monolizer/MonoMain.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package me.dynmie.monolizer;
22

33
import me.dynmie.monolizer.player.Asciifier;
4+
import me.dynmie.monolizer.player.ScanType;
45
import me.dynmie.monolizer.player.VideoPlayer;
56
import org.jline.terminal.Terminal;
67
import org.jline.terminal.TerminalBuilder;
@@ -17,6 +18,11 @@
1718
public class MonoMain {
1819

1920
public static void main(String[] args) throws IOException {
21+
// BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(FileDescriptor.out), 1080 * 720 * 22);
22+
//
23+
// PrintStream ps = new PrintStream(bos, false, "CP437"); // autoFlush = true
24+
// System.setOut(ps);
25+
2026
Terminal terminal = TerminalBuilder.terminal();
2127

2228
File sourceFile = new File("source.mp4");
@@ -52,7 +58,7 @@ public static void main(String[] args) throws IOException {
5258

5359
NonBlockingReader reader = terminal.reader();
5460

55-
VideoPlayer player = new VideoPlayer(System.out, sourceFile, width, height, new Asciifier(
61+
VideoPlayer player = new VideoPlayer(System.out, sourceFile, width, height, ScanType.PROGRESSIVE, new Asciifier(
5662
false,
5763
false,
5864
true,
@@ -106,6 +112,7 @@ public static void main(String[] args) throws IOException {
106112
player.pause();
107113
}
108114
}
115+
case 'i' -> player.setScanType(player.getScanType().next());
109116
case 'q' -> player.stop();
110117
}
111118
}

src/main/java/me/dynmie/monolizer/player/Asciifier.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public Asciifier(boolean color, boolean fullPixel, boolean textDithering, char[]
3434
this.brightnessLevels = brightnessLevels;
3535
}
3636

37-
public String createFrame(BufferedImage image) {
37+
public String[] createFrame(BufferedImage image) {
3838
int width = image.getWidth();
3939
int height = image.getHeight();
4040

@@ -50,6 +50,7 @@ public String createFrame(BufferedImage image) {
5050
int currentColor = image.getRGB(x, y);
5151

5252
boolean almostSameColor = isAlmostSameColor(currentColor, prevColor);
53+
// boolean almostSameColor = false;
5354

5455
String pixel = createPixel(width, height, x, y, currentColor, almostSameColor, textDitheringErrors);
5556
builder.append(pixel);
@@ -61,7 +62,7 @@ public String createFrame(BufferedImage image) {
6162

6263
lines[y] = builder.toString();
6364
});
64-
return String.join("\n", lines);
65+
return lines;
6566
}
6667

6768
private String createPixel(int width, int height, int x, int y, int currentColor, boolean almostSameColor, float[][] textDitheringErrors) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package me.dynmie.monolizer.player;
2+
3+
/**
4+
* @author dynmie
5+
*/
6+
public enum ScanType {
7+
PROGRESSIVE,
8+
INTERLACED,
9+
INTERLACED_NO_PREV;
10+
11+
public ScanType next() {
12+
return values()[(ordinal() + 1) % values().length];
13+
}
14+
}

src/main/java/me/dynmie/monolizer/player/VideoPlayer.java

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,34 @@ public class VideoPlayer {
2727
private final File source;
2828
private volatile int width;
2929
private volatile int height;
30+
private volatile ScanType scanType;
3031
private volatile Asciifier asciifier;
3132

3233
private volatile boolean running = false;
3334
private volatile boolean paused = false;
3435

3536
private Thread thread;
3637

37-
public VideoPlayer(OutputStream outputStream, File source, int width, int height, Asciifier asciifier) {
38+
private long frameCount = 0;
39+
private long lastFrameWriteDelayMicros = 0;
40+
41+
public VideoPlayer(OutputStream outputStream, File source, int width, int height, ScanType scanType, Asciifier asciifier) {
3842
this.outputStream = outputStream;
3943
this.source = source;
4044
this.width = width;
4145
this.height = height;
46+
this.scanType = scanType;
4247
this.asciifier = asciifier;
4348
}
4449

50+
public ScanType getScanType() {
51+
return scanType;
52+
}
53+
54+
public void setScanType(ScanType scanType) {
55+
this.scanType = scanType;
56+
}
57+
4558
public void setResolution(int width, int height) {
4659
this.width = width;
4760
this.height = height;
@@ -180,15 +193,9 @@ private void run() {
180193
BufferedImage image = converter.convert(imageFrame);
181194
imageFrame.close();
182195

183-
String prefix = "";
184-
if (!asciifier.isColor()) {
185-
prefix += ConsoleUtils.getForegroundResetCode();
186-
}
187-
if (!asciifier.isFullPixel()) {
188-
prefix += ConsoleUtils.getBackgroundResetCode();
189-
}
190196

191-
String text = prefix + ConsoleUtils.getResetCursorPositionEscapeCode() + asciifier.createFrame(image);
197+
198+
String[] text = asciifier.createFrame(image) ;
192199

193200
atomicQueueSize.incrementAndGet();
194201
imageExecutor.submit(() -> {
@@ -203,7 +210,7 @@ private void run() {
203210
if (delayMicros < 0 && queueSize > 1) return; // we're behind! skip the frame.
204211

205212
// recalculate delta
206-
delayMicros = imageFrame.timestamp - playbackTimer.elapsedMicros();
213+
delayMicros = imageFrame.timestamp - playbackTimer.elapsedMicros() - lastFrameWriteDelayMicros;
207214
// if video is faster than audio
208215
if (delayMicros > 0) {
209216
// wait for audio to catch up with the video
@@ -228,8 +235,25 @@ private void run() {
228235
}
229236
}
230237

231-
writer.write(text);
232-
writer.flush();
238+
long startTime = System.nanoTime();
239+
240+
String prefix = "";
241+
if (!asciifier.isColor()) {
242+
prefix += ConsoleUtils.getForegroundResetCode();
243+
}
244+
if (!asciifier.isFullPixel()) {
245+
prefix += ConsoleUtils.getBackgroundResetCode();
246+
}
247+
248+
writer.write(prefix + ConsoleUtils.getResetCursorPositionEscapeCode());
249+
250+
writeFrame(writer, text);
251+
252+
long endTime = System.nanoTime();
253+
254+
lastFrameWriteDelayMicros = TimeUnit.NANOSECONDS.toMicros(endTime - startTime);
255+
256+
frameCount++;
233257
});
234258
});
235259
} else if (frame.samples != null) { // if frame is audio frame
@@ -303,6 +327,50 @@ private void run() {
303327
paused = false;
304328
}
305329

330+
private void writeFrame(PrintWriter writer, String[] frame) {
331+
switch (scanType) {
332+
case INTERLACED -> writeInterlaced(writer, frame, false);
333+
case INTERLACED_NO_PREV -> writeInterlaced(writer, frame, true);
334+
case PROGRESSIVE -> writeProgressive(writer, frame);
335+
default -> throw new IllegalStateException("Unexpected value: " + scanType);
336+
}
337+
}
338+
339+
private void writeInterlaced(PrintWriter writer, String[] frame, boolean clearPrevious) {
340+
boolean even = (frameCount % 2) == 0;
341+
342+
for (int i = 0; i < frame.length; i++) {
343+
boolean lineEven = (i % 2 == 0);
344+
345+
boolean shouldNewLine = !((i + 1) % frame.length == 0);
346+
347+
if (even == lineEven) {
348+
writer.write(frame[i]);
349+
if (shouldNewLine) {
350+
writer.write("\n");
351+
}
352+
} else if (shouldNewLine) {
353+
if (!clearPrevious) {
354+
writer.write("\033[B");
355+
} else {
356+
writer.write(ConsoleUtils.getBackgroundResetCode() + "\033[2K\n");
357+
}
358+
}
359+
}
360+
361+
writer.flush();
362+
}
363+
364+
private void writeProgressive(PrintWriter writer, String[] frame) {
365+
for (int i = 0; i < frame.length; i++) {
366+
writer.write(frame[i]);
367+
if (!((i + 1) % frame.length == 0)) {
368+
writer.write("\n");
369+
}
370+
}
371+
writer.flush();
372+
}
373+
306374
private void createThread() {
307375
if (running) {
308376
throw new IllegalStateException("This getVideos player is already running!");

0 commit comments

Comments
 (0)