VideoClock uses divider in order to avoid too high host frequencies, so to avoid overloading the network

This commit is contained in:
ppeccin 2018-02-13 20:56:01 -02:00
parent fc43514870
commit 016bab1043
6 changed files with 282 additions and 239 deletions

View file

@ -8,9 +8,12 @@ jt.AtariConsole = function(mainVideoClock) {
function init() {
mainComponentsCreate();
socketsCreate();
setDefaults();
}
this.socketsConnected = function() {
setDefaults();
};
this.powerOn = function() {
if (this.powerIsOn) this.powerOff();
bus.powerOn();
@ -66,28 +69,6 @@ jt.AtariConsole = function(mainVideoClock) {
return systemPaused;
};
this.videoClockPulseOLD = function(ignoreSystemPause) {
if (systemPaused && !ignoreSystemPause) return;
if (!self.powerIsOn) return;
if (videoPulldown.steps === 1) {
// Simple pulldown with 1:1 cadence
videoFrame();
} else {
// Complex pulldown
var pulls = videoPulldown.cadence[--videoPulldownStep];
if (videoPulldownStep === 0) videoPulldownStep = videoPulldown.steps;
while(pulls > 0) {
pulls--;
videoFrame();
}
}
// Finish audio signal (generate any missing samples to adjust to sample rate)
if (!systemPaused && !userPaused) audioSocket.audioFinishFrame();
};
this.videoClockPulse = function() {
// Video clock will be the Tia Frame video clock (60Hz/50Hz)
// CPU and other clocks (Pia, Audio) will be sent by the Tia
@ -139,6 +120,10 @@ jt.AtariConsole = function(mainVideoClock) {
return saveStateSocket;
};
this.getVideoClockSocket = function() {
return videoClockSocket;
};
this.getAudioSocket = function() {
return audioSocket;
};
@ -242,14 +227,14 @@ jt.AtariConsole = function(mainVideoClock) {
// According to the native video frequency detected, target Video Standard and vSynchMode, use a specific pulldown configuration
if (vSynchMode === 1) { // ON
// Will V-synch to host freq if detected and supported, or use optimal timer configuration)
videoPulldown = videoStandard.pulldowns[jt.Clock.HOST_NATIVE_FPS] || videoStandard.pulldowns.TIMER;
videoPulldown = videoStandard.pulldowns[videoClockSocket.getVSynchNativeFrequency()] || videoStandard.pulldowns.TIMER;
} else { // OFF, DISABLED
// No V-synch. Always use the optimal timer configuration)
videoPulldown = videoStandard.pulldowns.TIMER;
}
videoPulldownStep = 0;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
//console.error("Update Synchronization: " + videoPulldown.frequency);
}
@ -289,7 +274,7 @@ jt.AtariConsole = function(mainVideoClock) {
if (s.upf !== undefined) userPauseMoreFrames = s.upf;
// Normal
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
tia.loadState(s.t);
pia.loadState(s.p);
ram.loadState(s.r);
@ -307,16 +292,16 @@ jt.AtariConsole = function(mainVideoClock) {
setVideoStandardAuto(true);
speedControl = 1;
alternateSpeed = null;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
tia.debug(0);
tia.debugNoCollisions(false);
}
function mainVideoClockUpdateSpeed() {
var freq = videoPulldown.frequency;
mainVideoClock.setVSynch(vSynchMode === 1);
mainVideoClock.setFrequency((freq * (alternateSpeed || speedControl)) | 0);
audioSocket.setFps(freq);
function videoClockUpdateSpeed() {
videoClockSocket.setVSynch(vSynchMode === 1);
var hostFreq = (videoPulldown.frequency * (alternateSpeed || speedControl)) | 0;
videoClockSocket.setFrequency(hostFreq, videoPulldown.divider);
audioSocket.setFps(hostFreq / videoPulldown.divider);
}
var mainComponentsCreate = function() {
@ -329,6 +314,7 @@ jt.AtariConsole = function(mainVideoClock) {
};
var socketsCreate = function() {
videoClockSocket = new VideoClockSocket();
consoleControlsSocket = new ConsoleControlsSocket();
cartridgeSocket = new CartridgeSocket();
saveStateSocket = new SaveStateSocket();
@ -356,6 +342,7 @@ jt.AtariConsole = function(mainVideoClock) {
var videoStandard;
var videoPulldown, videoPulldownStep;
var videoClockSocket;
var consoleControlsSocket;
var cartridgeSocket;
var saveStateSocket;
@ -382,11 +369,11 @@ jt.AtariConsole = function(mainVideoClock) {
if (control === controls.FAST_SPEED) {
if (state && alternateSpeed !== SPEED_FAST) {
alternateSpeed = SPEED_FAST;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
self.showOSD("FAST FORWARD", true);
} else if (!state && alternateSpeed === SPEED_FAST) {
alternateSpeed = null;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
self.showOSD(null, true);
}
return;
@ -394,11 +381,11 @@ jt.AtariConsole = function(mainVideoClock) {
if (control === controls.SLOW_SPEED) {
if (state && alternateSpeed !== SPEED_SLOW) {
alternateSpeed = SPEED_SLOW;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
self.showOSD("SLOW MOTION", true);
} else if (!state && alternateSpeed === SPEED_SLOW) {
alternateSpeed = null;
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
self.showOSD(null, true);
}
return;
@ -435,7 +422,7 @@ jt.AtariConsole = function(mainVideoClock) {
else if (control === controls.NORMAL_SPEED) speedIndex = SPEEDS.indexOf(1);
speedControl = SPEEDS[speedIndex];
self.showOSD("Speed: " + ((speedControl * 100) | 0) + "%", true);
mainVideoClockUpdateSpeed();
videoClockUpdateSpeed();
break;
case controls.SAVE_STATE_0: case controls.SAVE_STATE_1: case controls.SAVE_STATE_2: case controls.SAVE_STATE_3: case controls.SAVE_STATE_4: case controls.SAVE_STATE_5:
case controls.SAVE_STATE_6: case controls.SAVE_STATE_7: case controls.SAVE_STATE_8: case controls.SAVE_STATE_9: case controls.SAVE_STATE_10: case controls.SAVE_STATE_11: case controls.SAVE_STATE_12:
@ -479,6 +466,25 @@ jt.AtariConsole = function(mainVideoClock) {
};
// Video Clock Socket -----------------------------------------
function VideoClockSocket() {
this.connectClock = function(clock) {
videoClock = clock;
};
this.getVSynchNativeFrequency = function() {
return videoClock.getVSynchNativeFrequency();
};
this.setVSynch = function(state) {
videoClock.setVSynch(state);
};
this.setFrequency = function(freq, div) {
videoClock.setFrequency(freq, div);
};
var videoClock;
}
// CartridgeSocket -----------------------------------------
function CartridgeSocket() {
@ -712,20 +718,6 @@ jt.AtariConsole = function(mainVideoClock) {
// Debug methods ------------------------------------------------------
this.runFramesAtTopSpeed = function(frames) {
mainVideoClock.pause();
var start = jt.Util.performanceNow();
for (var i = 0; i < frames; i++) {
//var pulseTime = jt.Util.performanceNow();
self.videoClockPulseApplyPulldowns(1);
//console.log(jt.Util.performanceNow() - pulseTime);
}
var duration = jt.Util.performanceNow() - start;
jt.Util.log("Done running " + frames + " frames in " + (duration | 0) + " ms");
jt.Util.log((frames / (duration/1000)).toFixed(2) + " frames/sec");
mainVideoClock.go();
};
this.eval = function(str) {
return eval(str);
};

View file

@ -1,160 +0,0 @@
// Copyright 2015 by Paulo Augusto Peccin. See license.txt distributed with this file.
// Clock Pulse generator. Intended to be synchronized with Host machine Video Frequency whenever possible
jt.Clock = function(clockPulse) {
"use strict";
this.go = function() {
if (!running) {
//lastPulseTime = jt.Util.performanceNow();
//timeMeasures = [];
useRequestAnimationFrame = vSynch && (cyclesPerSecond === jt.Clock.HOST_NATIVE_FPS);
running = true;
if (useRequestAnimationFrame)
animationFrame = requestAnimationFrame(pulse);
else
interval = setInterval(pulse, cycleTimeMs);
}
};
this.pause = function() {
running = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
if (interval) {
clearInterval(interval);
interval = null;
}
};
this.isRunning = function() {
return running;
};
this.getFrequency = function() {
return cyclesPerSecond;
};
this.setFrequency = function(freq) {
if (running) {
this.pause();
internalSetFrequency(freq);
this.go();
} else {
internalSetFrequency(freq);
}
};
this.setVSynch = function(boo) {
if (running) {
this.pause();
vSynch = boo;
this.go();
} else {
vSynch = boo;
}
};
var internalSetFrequency = function(freq) {
cyclesPerSecond = freq;
cycleTimeMs = 1000 / freq;
};
var pulse = function() {
// var pulseTime = jt.Util.performanceNow();
// timeMeasures[timeMeasures.length] = pulseTime - lastPulseTime;
// var lastPulseTime = pulseTime;
animationFrame = null;
clockPulse();
if (useRequestAnimationFrame && !animationFrame)
animationFrame = requestAnimationFrame(pulse);
// console.log(jt.Util.performanceNow() - pulseTime);
};
//this.getMeasures = function() {
// return timeMeasures;
//};
this.eval = function(str) {
return eval(str);
};
var running = false;
var cyclesPerSecond = 1;
var cycleTimeMs = 1000;
var useRequestAnimationFrame;
var animationFrame = null;
var interval = null;
var vSynch = true;
//var timeMeasures = [];
//var lastPulseTime = 0;
};
jt.Clock.HOST_NATIVE_FPS = Javatari.SCREEN_VSYNCH_MODE === -1 ? -1 : Javatari.SCREEN_FORCE_HOST_NATIVE_FPS; // -1 = Unknown or not detected
jt.Clock.detectHostNativeFPSAndCallback = function(callback) {
if (Javatari.SCREEN_VSYNCH_MODE === -1) {
jt.Util.warning("Video native V-Synch disabled in configuration");
if (callback) callback(jt.Clock.HOST_NATIVE_FPS);
return;
}
if (jt.Clock.HOST_NATIVE_FPS !== -1) {
jt.Util.warning("Host video frequency forced in configuration: " + jt.Clock.HOST_NATIVE_FPS);
if (callback) callback(jt.Clock.HOST_NATIVE_FPS);
return;
}
// Start detection
var tries = 0;
var samples = [];
var lastTime = 0;
var good60 = 0, good50 = 0, good120 = 0, good100 = 0;
var tolerance = 0.06;
var sampler = function() {
// Detected?
if (good60 >= 10 || good50 >= 10 || good120 >= 10 || good100 >= 10) {
jt.Clock.HOST_NATIVE_FPS = good60 >= 10 ? 60 : good50 >= 10 ? 50 : good120 >= 10 ? 120 : 100;
jt.Util.log("Video native frequency detected: " + jt.Clock.HOST_NATIVE_FPS + "Hz");
if (callback) callback(jt.Clock.HOST_NATIVE_FPS);
return;
}
tries++;
if (tries <= 50) {
var currentTime = jt.Util.performanceNow();
var sample = currentTime - lastTime;
samples[samples.length] = sample;
lastTime = currentTime;
if ((sample >= (1000 / 60) * (1 - tolerance)) && (sample <= (1000 / 60) * (1 + tolerance))) good60++;
if ((sample >= (1000 / 50) * (1 - tolerance)) && (sample <= (1000 / 50) * (1 + tolerance))) good50++;
if ((sample >= (1000 / 120) * (1 - tolerance)) && (sample <= (1000 / 120) * (1 + tolerance))) good120++;
if ((sample >= (1000 / 100) * (1 - tolerance)) && (sample <= (1000 / 100) * (1 + tolerance))) good100++;
requestAnimationFrame(sampler);
} else {
jt.Clock.HOST_NATIVE_FPS = -1;
jt.Util.warning("Could not detect video native frequency. V-Synch DISABLED!");
if (callback) callback(jt.Clock.HOST_NATIVE_FPS);
}
};
sampler();
};

View file

@ -13,40 +13,49 @@ jt.VideoStandard = {
60: { // Host at 60Hz
standard: "NTSC",
frequency: 60,
linesPerCycle: 262, // Normal 1:1 cadence. Exact V-synch to 60 Hz
firstStepCycleLinesAdjust: 0,
divider: 1,
cadence: [ 1 ],
steps: 1
},
120: { // Host at 120Hz
120: { // Host at 120Hz, clock / 2
standard: "NTSC",
frequency: 120,
linesPerCycle: 131, // 0:1 pulldown. 1 frame generated each 2 frames shown
firstStepCycleLinesAdjust: 0,
divider: 2,
cadence: [ 1 ],
steps: 1
},
"120s": { // Host at 120Hz
standard: "NTSC",
frequency: 120,
divider: 1,
cadence: [ 0, 1 ],
steps: 2
},
50: { // Host at 50Hz
standard: "NTSC",
frequency: 50,
linesPerCycle: 314, // 1:1:1:1:2 pulldown. 6 frames generated each 5 frames shown
firstStepCycleLinesAdjust: +2,
divider: 1,
cadence: [ 1, 1, 1, 1, 2 ],
steps: 5
},
100: { // Host at 100Hz
100: { // Host at 100Hz, clock / 2
standard: "NTSC",
frequency: 100,
linesPerCycle: 157, // 0:1:0:1:1:0:1:0:1:1 pulldown. 6 frames generated each 10 frames shown
firstStepCycleLinesAdjust: +2,
divider: 2,
cadence: [ 1, 1, 1, 1, 2 ],
steps: 5
},
"100s": { // Host at 100Hz
standard: "NTSC",
frequency: 100,
divider: 1,
cadence: [ 0, 1, 0, 1, 1, 0, 1, 0, 1, 1 ],
steps: 10
},
TIMER: { // Host frequency not detected or V-synch disabled, use a normal interval timer
standard: "NTSC",
frequency: 62.5,
linesPerCycle: 262, // Normal 1:1 cadence
firstStepCycleLinesAdjust: 0,
divider: 1,
cadence: [ 1 ],
steps: 1
}
@ -64,40 +73,49 @@ jt.VideoStandard = {
50: { // Host at 50Hz
standard: "PAL",
frequency: 50,
linesPerCycle: 313, // Normal 1:1 cadence. Exact V-synch to 50 Hz
firstStepCycleLinesAdjust: 0,
divider: 1,
cadence: [ 1 ],
steps: 1
},
100: { // Host at 100Hz
100: { // Host at 100Hz, clock / 2
standard: "PAL",
frequency: 100,
linesPerCycle: 156, // 0:1 pulldown. 1 frame generated each 2 frames shown
firstStepCycleLinesAdjust: +1,
divider: 2,
cadence: [ 1 ],
steps: 1
},
"100s": { // Host at 100Hz
standard: "PAL",
frequency: 100,
divider: 1,
cadence: [ 0, 1 ],
steps: 2
},
60: { // Host at 60Hz
standard: "PAL",
frequency: 60,
linesPerCycle: 261, // 0:1:1:1:1:1 pulldown. 5 frames generated each 6 frames shown
firstStepCycleLinesAdjust: -1,
divider: 1,
cadence: [ 0, 1, 1, 1, 1, 1 ],
steps: 6
},
120: { // Host at 120Hz
120: { // Host at 120Hz, clock / 2
standard: "PAL",
frequency: 120,
linesPerCycle: 130, // 0:0:1:0:1:0:0:1:0:1:0:1 pulldown. 5 frames generated each 12 frames shown
firstStepCycleLinesAdjust: +5,
divider: 2,
cadence: [ 0, 1, 1, 1, 1, 1 ],
steps: 6
},
"120s": { // Host at 120Hz
standard: "PAL",
frequency: 120,
divider: 1,
cadence: [ 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1 ],
steps: 12
},
TIMER: { // Host frequency not detected or V-synch disabled, use a normal interval timer
standard: "PAL",
frequency: 50,
linesPerCycle: 313, // Normal 1:1 cadence
firstStepCycleLinesAdjust: 0,
divider: 1,
cadence: [ 1 ],
steps: 1
}

View file

@ -45,7 +45,7 @@ jt.Room = function(screenElement, consoleStartPowerOn) {
};
this.start = function(startAction) {
jt.Clock.detectHostNativeFPSAndCallback(function(nativeFPS) {
this.mainVideoClock.detectHostNativeFPSAndCallback(function(nativeFPS) {
self.console.vSynchSetSupported(nativeFPS > 0);
afterPowerONDelay(function () {
self.setLoading(false);
@ -154,14 +154,15 @@ jt.Room = function(screenElement, consoleStartPowerOn) {
}
function buildAndPlugConsole() {
self.console = new jt.AtariConsole(self.mainVideoClock);
self.console = new jt.AtariConsole();
self.mainVideoClock.connect(self.console.getVideoClockSocket());
self.stateMedia.connect(self.console.getSavestateSocket());
self.fileLoader.connect(self.console);
self.screen.connect(self.console);
self.speaker.connect(self.console.getAudioSocket());
self.consoleControls.connect(self.console.getConsoleControlsSocket());
self.peripheralControls.connect(self.console.getCartridgeSocket());
// Cartridge Data operations unavailable self.console.getCartridgeSocket().connectFileDownloader(self.fileDownloader);
self.console.socketsConnected();
}
@ -188,6 +189,23 @@ jt.Room = function(screenElement, consoleStartPowerOn) {
var roomPowerOnTime;
// Debug methods ------------------------------------------------------
this.runFramesAtTopSpeed = function(frames) {
this.mainVideoClock.pause();
var start = jt.Util.performanceNow();
for (var i = 0; i < frames; i++) {
//var pulseTime = jt.Util.performanceNow();
self.mainVideoClockPulse();
//console.log(jt.Util.performanceNow() - pulseTime);
}
var duration = jt.Util.performanceNow() - start;
jt.Util.log("Done running " + frames + " frames in " + (duration | 0) + " ms");
jt.Util.log((frames / (duration/1000)).toFixed(2) + " frames/sec");
this.mainVideoClock.go();
};
init();
};

View file

@ -0,0 +1,175 @@
// Copyright 2015 by Paulo Augusto Peccin. See license.txt distributed with this file.
// Clock Pulse generator. Intended to be synchronized with Host machine Video Frequency whenever possible
jt.Clock = function(clockPulse) {
"use strict";
this.connect = function(clockSocket) {
clockSocket.connectClock(this);
};
this.go = function() {
if (!running) {
//lastPulseTime = jt.Util.performanceNow();
//timeMeasures = [];
useRequestAnimationFrame = vSynch && (cyclesPerSecond === this.getVSynchNativeFrequency());
// console.log("Clock at " + cyclesPerSecond + " / " + divider + " using RequestAnimationFrame: " + useRequestAnimationFrame);
running = true;
if (useRequestAnimationFrame)
animationFrame = requestAnimationFrame(pulse);
else
interval = setInterval(pulse, cycleTimeMs);
}
};
this.pause = function() {
running = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
if (interval) {
clearInterval(interval);
interval = null;
}
};
this.setFrequency = function(freq, div) {
if (running) {
this.pause();
internalSetFrequency(freq, div);
this.go();
} else {
internalSetFrequency(freq, div);
}
};
this.setVSynch = function(state) {
if (running) {
this.pause();
vSynch = state;
this.go();
} else {
vSynch = state;
}
};
this.getVSynchNativeFrequency = function() {
return vSynchAltNativeFrequency || vSynchNativeFrequency;
};
this.setVSynchAltNativeFrequency = function(freq) {
vSynchAltNativeFrequency = freq;
};
var internalSetFrequency = function(freq, div) {
cyclesPerSecond = freq;
cycleTimeMs = 1000 / freq;
divider = div >= 1 ? div : 1;
if (dividerCounter > divider) dividerCounter = divider;
};
var pulse = function() {
//var pulseTime = jt.Util.performanceNow();
//timeMeasures[timeMeasures.length] = pulseTime - lastPulseTime;
//lastPulseTime = pulseTime;
animationFrame = null;
if (divider > 1) {
if (--dividerCounter <= 0) {
dividerCounter = divider;
clockPulse();
}
} else
clockPulse();
if (useRequestAnimationFrame && !animationFrame)
animationFrame = requestAnimationFrame(pulse);
//console.log(jt.Util.performanceNow() - pulseTime);
};
//this.getMeasures = function() {
// return timeMeasures;
//};
this.detectHostNativeFPSAndCallback = function(callback) {
if (Javatari.SCREEN_VSYNCH_MODE === -1) {
jt.Util.warning("Video native V-Synch disabled in configuration");
if (callback) callback(vSynchNativeFrequency);
return;
}
if (Javatari.SCREEN_FORCE_HOST_NATIVE_FPS !== -1) {
jt.Util.warning("Host video frequency forced in configuration: " + Javatari.SCREEN_FORCE_HOST_NATIVE_FPS);
if (callback) callback(vSynchNativeFrequency);
return;
}
// Start detection
var tries = 0;
var samples = [];
var lastTime = 0;
var good60 = 0, good50 = 0, good120 = 0, good100 = 0;
var tolerance = 0.06;
var nativeFPSSampler = function() {
// Detected?
if (good60 >= 10 || good50 >= 10 || good120 >= 10 || good100 >= 10) {
vSynchNativeFrequency = good60 >= 10 ? 60 : good50 >= 10 ? 50 : good120 >= 10 ? 120 : 100;
jt.Util.log("Video native frequency detected: " + vSynchNativeFrequency + "Hz");
if (callback) callback(vSynchNativeFrequency);
return;
}
tries++;
if (tries <= 50) {
var currentTime = jt.Util.performanceNow();
var sample = currentTime - lastTime;
samples[samples.length] = sample;
lastTime = currentTime;
if ((sample >= (1000 / 60) * (1 - tolerance)) && (sample <= (1000 / 60) * (1 + tolerance))) good60++;
if ((sample >= (1000 / 50) * (1 - tolerance)) && (sample <= (1000 / 50) * (1 + tolerance))) good50++;
if ((sample >= (1000 / 120) * (1 - tolerance)) && (sample <= (1000 / 120) * (1 + tolerance))) good120++;
if ((sample >= (1000 / 100) * (1 - tolerance)) && (sample <= (1000 / 100) * (1 + tolerance))) good100++;
requestAnimationFrame(nativeFPSSampler);
} else {
vSynchNativeFrequency = -1;
jt.Util.error("Could not detect video native frequency. V-Synch DISABLED!");
if (callback) callback(vSynchNativeFrequency);
}
};
nativeFPSSampler();
};
this.eval = function(str) {
return eval(str);
};
var running = false;
var cyclesPerSecond = 1;
var cycleTimeMs = 1000;
var divider = 1;
var dividerCounter = 1;
var useRequestAnimationFrame;
var animationFrame = null;
var interval = null;
var vSynch = true;
var vSynchNativeFrequency = Javatari.SCREEN_VSYNCH_MODE === -1 ? -1 : Javatari.SCREEN_FORCE_HOST_NATIVE_FPS; // -1 = Unknown or not detected
var vSynchAltNativeFrequency = undefined; // undefined = deactivated. Used by NetPlay to force the same frequency as the Server
//var timeMeasures = [];
//var lastPulseTime = 0;
};

View file

@ -61,7 +61,6 @@
<script src="../src/main/atari/tia/TiaAudio.js"></script>
<script src="../src/main/atari/tia/TiaAudioChannel.js"></script>
<script src="../src/main/atari/tia/Tia.js"></script>
<script src="../src/main/atari/console/Clock.js"></script>
<script src="../src/main/atari/console/Bus.js"></script>
<script src="../src/main/atari/console/AtariConsole.js"></script>
<script src="../src/main/atari/controls/JoystickButtons.js"></script>
@ -89,6 +88,7 @@
<script src="../src/main/atari/cartridge/CartridgeFormats.js"></script>
<script src="../src/main/atari/cartridge/CartridgeCreator.js"></script>
<script src="../src/main/images/Images.js"></script>
<script src="../src/main/room/clock/Clock.js"></script>
<script src="../src/main/room/files/RecentStoredROMs.js"></script>
<script src="../src/main/room/files/FileLoader.js"></script>
<script src="../src/main/room/files/FileDownloader.js"></script>