diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 0ae868f..ec638bd 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -86,6 +86,7 @@ import { EditCategoryDialogComponent } from './dialogs/edit-category-dialog/edit
import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.component';
import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component';
import { H401Interceptor } from './http.interceptor';
+import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component';
registerLocaleData(es, 'es');
@@ -134,7 +135,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
CustomPlaylistsComponent,
EditCategoryDialogComponent,
TwitchChatComponent,
- SeeMoreComponent
+ SeeMoreComponent,
+ ConcurrentStreamComponent
],
imports: [
CommonModule,
diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.html b/src/app/components/concurrent-stream/concurrent-stream.component.html
new file mode 100644
index 0000000..414c4ac
--- /dev/null
+++ b/src/app/components/concurrent-stream/concurrent-stream.component.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.scss b/src/app/components/concurrent-stream/concurrent-stream.component.scss
new file mode 100644
index 0000000..d3b74be
--- /dev/null
+++ b/src/app/components/concurrent-stream/concurrent-stream.component.scss
@@ -0,0 +1,7 @@
+.buttons-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 15px;
+ margin-bottom: 15px;
+}
\ No newline at end of file
diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts
new file mode 100644
index 0000000..a881ec8
--- /dev/null
+++ b/src/app/components/concurrent-stream/concurrent-stream.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ConcurrentStreamComponent } from './concurrent-stream.component';
+
+describe('ConcurrentStreamComponent', () => {
+ let component: ConcurrentStreamComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ConcurrentStreamComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConcurrentStreamComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/concurrent-stream/concurrent-stream.component.ts b/src/app/components/concurrent-stream/concurrent-stream.component.ts
new file mode 100644
index 0000000..6c2cc67
--- /dev/null
+++ b/src/app/components/concurrent-stream/concurrent-stream.component.ts
@@ -0,0 +1,140 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { PostsService } from 'app/posts.services';
+
+@Component({
+ selector: 'app-concurrent-stream',
+ templateUrl: './concurrent-stream.component.html',
+ styleUrls: ['./concurrent-stream.component.scss']
+})
+export class ConcurrentStreamComponent implements OnInit {
+
+ @Input() server_mode = false;
+ @Input() playback_timestamp;
+ @Input() playing;
+ @Input() uid;
+
+ @Output() setPlaybackTimestamp = new EventEmitter();
+ @Output() togglePlayback = new EventEmitter();
+ @Output() setPlaybackRate = new EventEmitter();
+
+ started = false;
+ server_started = false;
+ watch_together_clicked = false;
+
+ server_already_exists = null;
+
+ check_timeout: any;
+ update_timeout: any;
+
+ PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION = 0.5;
+ PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP = 2;
+
+ PLAYBACK_MODIFIER = 0.1;
+
+ playback_rate_modified = false;
+
+ constructor(private postsService: PostsService) { }
+
+ // flow: click start watching -> check for available stream to enable join button and if user, display "start stream"
+ // users who join a stream will send continuous requests for info on playback
+
+ ngOnInit(): void {
+
+ }
+
+ startServer() {
+ this.started = true;
+ this.server_started = true;
+ this.update_timeout = setInterval(() => {
+ this.updateStream();
+ }, 1000);
+ }
+
+ updateStream() {
+ this.postsService.updateConcurrentStream(this.uid, this.playback_timestamp, Date.now()/1000, this.playing).subscribe(res => {
+ });
+ }
+
+ startClient() {
+ this.started = true;
+ }
+
+ checkStream() {
+ if (this.server_started) { return; }
+ const current_playback_timestamp = this.playback_timestamp;
+ const current_unix_timestamp = Date.now()/1000;
+ this.postsService.checkConcurrentStream(this.uid).subscribe(res => {
+ const stream = res['stream'];
+
+ if (!stream) {
+ this.server_already_exists = false;
+ return;
+ }
+
+ this.server_already_exists = true;
+
+ // check whether client has joined the stream
+ if (!this.started) { return; }
+
+ if (!stream['playing'] && this.playing) {
+ // tell client to pause and set the timestamp to sync
+ this.togglePlayback.emit(false);
+ this.setPlaybackTimestamp.emit(stream['playback_timestamp']);
+ } else if (stream['playing']) {
+ // sync unpause state
+ if (!this.playing) { this.togglePlayback.emit(true); }
+
+ // sync time
+ const zeroed_local_unix_timestamp = current_unix_timestamp - current_playback_timestamp;
+ const zeroed_server_unix_timestamp = stream['unix_timestamp'] - stream['playback_timestamp'];
+
+ const seconds_behind_locally = zeroed_local_unix_timestamp - zeroed_server_unix_timestamp;
+
+ if (Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_SKIP) {
+ // skip to playback timestamp because the difference is too high
+ this.setPlaybackTimestamp.emit(this.playback_timestamp + seconds_behind_locally + 0.3);
+ this.playback_rate_modified = false;
+ } else if (!this.playback_rate_modified && Math.abs(seconds_behind_locally) > this.PLAYBACK_TIMESTAMP_DIFFERENCE_THRESHOLD_PLAYBACK_MODIFICATION) {
+ // increase playback speed to avoid skipping
+ let seconds_to_wait = (Math.abs(seconds_behind_locally)/this.PLAYBACK_MODIFIER);
+ seconds_to_wait += 0.3/this.PLAYBACK_MODIFIER;
+
+ this.playback_rate_modified = true;
+
+ if (seconds_behind_locally > 0) {
+ // increase speed
+ this.setPlaybackRate.emit(1 + this.PLAYBACK_MODIFIER);
+ setTimeout(() => {
+ this.setPlaybackRate.emit(1);
+ this.playback_rate_modified = false;
+ }, seconds_to_wait * 1000);
+ } else {
+ // decrease speed
+ this.setPlaybackRate.emit(1 - this.PLAYBACK_MODIFIER);
+ setTimeout(() => {
+ this.setPlaybackRate.emit(1);
+ this.playback_rate_modified = false;
+ }, seconds_to_wait * 1000);
+ }
+ }
+ }
+ });
+ }
+
+ startWatching() {
+ this.watch_together_clicked = true;
+ this.check_timeout = setInterval(() => {
+ this.checkStream();
+ }, 1000);
+ }
+
+ stop() {
+ if (this.check_timeout) { clearInterval(this.check_timeout); }
+ if (this.update_timeout) { clearInterval(this.update_timeout); }
+ this.started = false;
+ this.server_started = false;
+ this.watch_together_clicked = false;
+ }
+
+
+}