/*
 * test_av_play.c - Test playing an MPEG A+V PES (e.g. VDR recordings) from a file.
 *
 * Copyright (C) 2000 Ralph  Metzler <ralph@convergence.de>
 *                  & Marcus Metzler <marcus@convergence.de>
 *                    for convergence integrated media GmbH
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Thu Jun 24 09:18:44 CEST 2004
 *   Add scan_file_av() and copy_to_dvb() for AV
 *   filtering to be able to use AC3 audio streams
 *   Copyright (C) 2004 Werner Fink <werner@suse.de>
 */

#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif

#include <sys/ioctl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <time.h>
#include <termios.h>
#include <unistd.h>
#include <errno.h>

#include <linux/dvb/dmx.h>
#include <linux/dvb/video.h>
#include <linux/dvb/audio.h>
#include <sys/poll.h>

static char dolby;
static char audio;
static char black;
static char volset;

static int audioPlay(int fd)
{
	int ans;

	if ((ans = ioctl(fd,AUDIO_PLAY)) < 0) {
		perror("AUDIO PLAY: ");
		return -1;
	}

	return 0;
}

static int audioSelectSource(int fd, audio_stream_source_t source)
{
	int ans;

	if ((ans = ioctl(fd,AUDIO_SELECT_SOURCE, source)) < 0) {
		perror("AUDIO SELECT SOURCE: ");
		return -1;
	}

	return 0;
}

static int audioSetMute(int fd, int state)
{
	int ans;

	if ((ans = ioctl(fd,AUDIO_SET_MUTE, state)) < 0) {
		perror("AUDIO SET MUTE: ");
		return -1;
	}

	return 0;
}

static int audioSetAVSync(int fd, int state)
{
	int ans;

	if ((ans = ioctl(fd,AUDIO_SET_AV_SYNC, state)) < 0) {
		perror("AUDIO SET AV SYNC: ");
		return -1;
	}

	return 0;
}

static int audioSetVolume(int fd, int level)
{
	int ans;
	audio_mixer_t mix;

	mix.volume_left = mix.volume_right = level;
	if ((ans = ioctl(fd, AUDIO_SET_MIXER, &mix)) < 0) {
		perror("AUDIO SET VOLUME: ");
		return -1;
	}
	return 0;
}

static int audioStop(int fd)
{
	int ans;

	if ((ans = ioctl(fd,AUDIO_STOP,0)) < 0) {
		perror("AUDIO STOP: ");
		return -1;
	}
	return 0;
}

static int deviceClear(int afd, int vfd)
{
	int ans;

	if (vfd >= 0) {
		if ((ans = ioctl(vfd, VIDEO_CLEAR_BUFFER, 0)) < 0) {
			perror("VIDEO CLEAR BUFFER: ");
			return -1;
		}
	}
	if (afd >= 0) {
		if ((ans = ioctl(afd, AUDIO_CLEAR_BUFFER, 0)) < 0) {
			perror("AUDIO CLEAR BUFFER: ");
			return -1;
		}
	}

	return 0;
}

static int videoStop(int fd)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_STOP,0)) < 0) {
		perror("VIDEO STOP: ");
		return -1;
	}

	return 0;
}

static int videoPlay(int fd)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_PLAY)) < 0) {
		perror("VIDEO PLAY: ");
		return -1;
	}

	return 0;
}

static int videoFreeze(int fd)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_FREEZE)) < 0) {
		perror("VIDEO FREEZE: ");
		return -1;
	}

	return 0;
}

static int videoBlank(int fd, int state)
{
	int ans;

	if ((ans = ioctl(fd, VIDEO_SET_BLANK, state)) < 0) {
		perror("VIDEO BLANK: ");
		return -1;
	}
	return 0;
}

static int videoContinue(int fd)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_CONTINUE)) < 0) {
		perror("VIDEO CONTINUE: ");
		return -1;
	}

	return 0;
}

static int videoSelectSource(int fd, video_stream_source_t source)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_SELECT_SOURCE, source)) < 0) {
		perror("VIDEO SELECT SOURCE: ");
		return -1;
	}

	return 0;
}


static int videoFastForward(int fd,int nframes)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_FAST_FORWARD, nframes)) < 0) {
		perror("VIDEO FAST FORWARD: ");
		return -1;
	}

	return 0;
}

static int videoSlowMotion(int fd,int nframes)
{
	int ans;

	if ((ans = ioctl(fd,VIDEO_SLOWMOTION, nframes)) < 0) {
		perror("VIDEO SLOWMOTION: ");
		return -1;
	}

	return 0;
}

#define NFD   3
static void copy_to_dvb(int vfd, int afd, int cfd, const uint8_t* ptr, const unsigned short len)
{
	struct pollfd pfd[NFD];
	unsigned short pos = 0;
	int stopped = 0;

	pfd[0].fd = STDIN_FILENO;
	pfd[0].events = POLLIN;

	pfd[1].fd = vfd;
	pfd[1].events = POLLOUT;

	pfd[2].fd = afd;
	pfd[2].events = POLLOUT;

	while (pos < len) {
		int ret;
		if ((ret = poll(pfd,NFD,1)) > 0) {
			if (pfd[1].revents & POLLOUT) {
				int cnt = write(cfd, ptr + pos, len - pos);
				if (cnt > 0)
					pos += cnt;
				else if (cnt < 0) {
					if (errno != EAGAIN && errno != EINTR) {
						perror("Write:");
						exit(-1);
					}
					if (errno == EAGAIN)
						usleep(1000);
					continue;
				}
			}
			if (pfd[0].revents & POLLIN) {
				int c = getchar();
				switch(c) {
				case 'z':
					if (audio && !black) {
						audioSetMute(afd, 1);
					} else {
						videoFreeze(vfd);
					}
					deviceClear(afd, -1);
					printf("playback frozen\n");
					stopped = 1;
					break;

				case 's':
					if (audio) {
						audioStop(afd);
						deviceClear(afd, -1);
					} else {
						videoStop(vfd);
						deviceClear(afd, vfd);
					}
					printf("playback stopped\n");
					stopped = 1;
					break;

				case 'c':
					if (audio && !black) {
						audioSetAVSync(afd, 0);
						deviceClear(afd, -1);
						audioSetMute(afd, 0);
					} else {
						audioSetAVSync(afd, 1);
						deviceClear(afd, vfd);
						videoContinue(vfd);
					}
					printf("playback continued\n");
					stopped = 0;
					break;

				case 'p':
					if (audio) {
						deviceClear(afd, -1);
						audioSetAVSync(afd, 0);
						audioPlay(afd);
					} else {
						deviceClear(afd, vfd);
						audioSetAVSync(afd, 1);
						audioPlay(afd);
						videoPlay(vfd);
					}
					audioSetMute(afd, 0);
					printf("playback started\n");
					stopped = 0;
					break;

				case 'f':
					audioSetAVSync(afd, 0);
					if (!audio) {
						audioSetMute(afd, 1);
						videoFastForward(vfd,0);
					}
					printf("fastforward\n");
					stopped = 0;
					break;

				case 'm':
					audioSetAVSync(afd, 0);
					audioSetMute(afd, 1);
					printf("mute\n");
					stopped = 0;
					break;

				case 'u':
					audioSetAVSync(afd, 1);
					audioSetMute(afd, 0);
					printf("unmute\n");
					stopped = 0;
					break;

				case 'd':
					if (dolby)
						dolby = 0;
					else
						dolby++;
					break;

				case 'l':
					audioSetAVSync(afd, 0);
					if (!audio) {
						audioSetMute(afd, 1);
						videoSlowMotion(vfd,2);
					}
					printf("slowmotion\n");
					stopped = 0;
					break;

				case 'q':
					videoContinue(vfd);
					exit(0);
					break;

				default:
					break;
				}
			}
		} else if (ret < 0) {
			if (errno != EAGAIN && errno != EINTR) {
				perror("Write:");
				exit(-1);
			}
			if (errno == EAGAIN)
				usleep(1000);
		}
	}
}

static unsigned char play[6] = {0x00, 0x00, 0x01, 0xff, 0xff, 0xff};
static unsigned char except[2];

static int scan_file_av(int vfd, int afd, const unsigned char *buf, int buflen)
{
	const unsigned char *const start = buf;
	const unsigned char *const   end = buf + buflen;

	static unsigned int magic = 0xffffffff;
	static unsigned short count, len;
	static int fdc = -1;
	int m;

	while (buf < end) {
		if (count < 6) {
			switch (count) {
			case 0:
				m = 0;
				while ((magic & 0xffffff00) != 0x00000100) {
					if (buf >= end) goto out;
					magic = (magic << 8) | *buf++;
					m++;
				}
				if (m > 4)
					printf("Broken Frame found\n");
				play[3] = (unsigned char)(magic & 0x000000ff);
				switch (play[3]) {
				case 0xE0 ... 0xEF:
					fdc = vfd;
					if (except[0] != play[3]) {
						if (except[0] == 0)
							except[0] = play[3];
						else
							fdc = -1;
					}
					if (audio)
						fdc = -1;
					break;
				case 0xC0 ... 0xDF:
					fdc = afd;
					if (dolby)
						fdc = -1;
					if (except[1] != play[3]) {
						if (except[1] == 0)
							except[1] = play[3];
						else
							fdc = -1;
					}
					if (!volset) {
						audioSetVolume(afd, 255);
						volset = 1;
					}
					break;
				case 0xBD:
					/*
					 * TODO: sub filter to through out e.g. ub pictures
					 * in Private Streams 1 _and_ get sub audio header
					 * to set an except(ion) audio stream.
					 * The later one requires some changes within the VDR
					 * remux part!  2004/07/01 Werner
					 */
					fdc = afd;
					if (!dolby)
						fdc = -1;
					if (volset) {
						audioSetVolume(afd, 0);
						volset = 0;
					}
					break;
				default:
					fdc = -1;
					break;
				}
				count = 4;
			case 4:
				if (buf >= end) goto out;
				len = ((*buf) << 8);
				play[4] = (*buf);
				buf++;
				count++;
			case 5:
				if (buf >= end) goto out;
				len |= (*buf);
				len += 6;
				play[5] = (*buf);
				buf++;
				count++;
				if (fdc != -1)
					copy_to_dvb(vfd, afd, fdc, &play[0], count);
			default:
				break;
			}
		}

		while (count < len) {
			int rest = end - buf;
			if (rest <= 0) goto out;

			if (rest + count > len)
				rest = len - count;

			if (fdc != -1)
				copy_to_dvb(vfd, afd, fdc, buf, rest);
			count += rest;
			buf   += rest;
		}

		/* Reset for next scan */
		magic = 0xffffffff;
		count = len = 0;
		fdc = -1;
		play[3] = 0xff;
	}
out:
	return buf - start;
}

#define BUFFY 32768
static void play_file_av(int filefd, int vfd, int afd)
{
	unsigned char buf[BUFFY];
	int count;

	audioSetMute(afd, 1);
	videoBlank(vfd, 1);
	if (audio && !black) {
		audioStop(afd);
		deviceClear(afd, -1);
		audioSetAVSync(afd, 0);
		audioSelectSource(afd, AUDIO_SOURCE_MEMORY);
		audioPlay(afd);
		videoBlank(vfd, 0);
	} else if (audio && black) {
		deviceClear(afd, vfd);
		videoBlank(vfd, 1);
		audioSetAVSync(afd, 0);
		audioSelectSource(afd, AUDIO_SOURCE_MEMORY);
		videoSelectSource(vfd, VIDEO_SOURCE_MEMORY);
		audioPlay(afd);
		videoPlay(vfd);
	} else {
		deviceClear(afd, vfd);
		audioSetAVSync(afd, 1);
		audioSelectSource(afd, AUDIO_SOURCE_MEMORY);
		videoSelectSource(vfd, VIDEO_SOURCE_MEMORY);
		audioPlay(afd);
		videoPlay(vfd);
		videoBlank(vfd, 0);
	}

	if (dolby) {
		audioSetVolume(afd, 0);
		volset = 0;
	} else {
		audioSetVolume(afd, 255);
		volset = 1;
	}

#ifndef __stub_posix_fadvise
	posix_fadvise(filefd, 0, 0, POSIX_FADV_SEQUENTIAL);
#endif

	while ((count = read(filefd,buf,BUFFY)) > 0)
		scan_file_av(vfd,afd,buf,count);
}

static struct termios term;

static void restore(void)
{
	tcsetattr(STDIN_FILENO, TCSANOW, &term);
}

int main(int argc, char **argv)
{
	int vfd, afd, c;
	int filefd;
	const char *videodev = "/dev/dvb/adapter0/video0";
	const char *audiodev = "/dev/dvb/adapter0/audio0";

	if (((tcgetpgrp(STDIN_FILENO) == getpid()) || (getppid() != (pid_t)1))
	    && (tcgetattr(STDIN_FILENO, &term) == 0)) {
		struct termios newterm;
		memcpy(&newterm, &term, sizeof(struct termios));
		newterm.c_iflag = 0;
		newterm.c_lflag &= ~(ICANON | ECHO);
		newterm.c_cc[VMIN] = 0;
		newterm.c_cc[VTIME] = 0;
		atexit(restore);
		tcsetattr(STDIN_FILENO, TCSANOW, &newterm);
	}

	opterr = 0;
	while ((c = getopt(argc, argv, "+daA")) != -1) {
		switch (c) {
		case 'd':
			dolby++;
			break;
		case 'a':
			audio++;
			break;
		case 'A':
			audio++;
			black++;
			break;
		case '?':
			fprintf(stderr, "usage: test_av_play [-d] [-a] [-A] mpeg_A+V_PES_file\n");
			return 1;
		default:
			break;
		}
	}
	argv += optind;
	argc -= optind;

	if (getenv("VIDEO"))
		videodev = getenv("VIDEO");
	if (getenv("AUDIO"))
		audiodev = getenv("AUDIO");

	printf("using video device '%s'\n", videodev);
	printf("using audio device '%s'\n", audiodev);

	putchar('\n');

	printf("Freeze       by pressing `z'\n");
	printf("Stop         by pressing `s'\n");
	printf("Continue     by pressing `c'\n");
	printf("Start        by pressing `p'\n");
	printf("FastForward  by pressing `f'\n");
	printf("Mute         by pressing `m'\n");
	printf("UnMute       by pressing `u'\n");
	printf("MP2/AC3      by pressing `d'\n");
	printf("SlowMotion   by pressing `l'\n");
	printf("Quit         by pressing `q'\n");

	putchar('\n');

	errno = ENOENT;
	if (!argv[0] || (filefd = open(argv[0], O_RDONLY)) < 0) {
		perror("File open:");
		return -1;
	}
	if ((vfd = open(videodev,O_RDWR|O_NONBLOCK)) < 0) {
		perror("VIDEO DEVICE: ");
		return -1;
	}
	if ((afd = open(audiodev,O_RDWR|O_NONBLOCK)) < 0) {
		perror("AUDIO DEVICE: ");
		return -1;
	}

	play_file_av(filefd, vfd, afd);
	close(vfd);
	close(afd);
	close(filefd);
	return 0;
}
