File size: 4,743 Bytes
11452ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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<any>();
  @Output() togglePlayback = new EventEmitter<boolean>();
  @Output() setPlaybackRate = new EventEmitter<number>();

  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 {

  }

  ngOnDestroy(): void {
    if (this.check_timeout) { clearInterval(this.check_timeout); }
    if (this.update_timeout) { clearInterval(this.update_timeout); }
  }

  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;
  }


}