diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 12 | ||||
-rw-r--r-- | server/helpers/requests.ts | 2 | ||||
-rw-r--r-- | server/helpers/utils.ts | 1 | ||||
-rw-r--r-- | server/lib/activitypub/actor.ts | 4 | ||||
-rw-r--r-- | server/lib/hls.ts | 136 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 5 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 4 | ||||
-rw-r--r-- | server/tests/api/redundancy/redundancy.ts | 22 | ||||
-rw-r--r-- | server/tests/api/videos/video-hls.ts | 16 |
9 files changed, 132 insertions, 70 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 5ad8ed48e..133b1b03b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -122,7 +122,9 @@ type TranscodeOptions = { | |||
122 | resolution: VideoResolution | 122 | resolution: VideoResolution |
123 | isPortraitMode?: boolean | 123 | isPortraitMode?: boolean |
124 | 124 | ||
125 | generateHlsPlaylist?: boolean | 125 | hlsPlaylist?: { |
126 | videoFilename: string | ||
127 | } | ||
126 | } | 128 | } |
127 | 129 | ||
128 | function transcode (options: TranscodeOptions) { | 130 | function transcode (options: TranscodeOptions) { |
@@ -161,14 +163,16 @@ function transcode (options: TranscodeOptions) { | |||
161 | command = command.withFPS(fps) | 163 | command = command.withFPS(fps) |
162 | } | 164 | } |
163 | 165 | ||
164 | if (options.generateHlsPlaylist) { | 166 | if (options.hlsPlaylist) { |
165 | const segmentFilename = `${dirname(options.outputPath)}/${options.resolution}_%03d.ts` | 167 | const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` |
166 | 168 | ||
167 | command = command.outputOption('-hls_time 4') | 169 | command = command.outputOption('-hls_time 4') |
168 | .outputOption('-hls_list_size 0') | 170 | .outputOption('-hls_list_size 0') |
169 | .outputOption('-hls_playlist_type vod') | 171 | .outputOption('-hls_playlist_type vod') |
170 | .outputOption('-hls_segment_filename ' + segmentFilename) | 172 | .outputOption('-hls_segment_filename ' + videoPath) |
173 | .outputOption('-hls_segment_type fmp4') | ||
171 | .outputOption('-f hls') | 174 | .outputOption('-f hls') |
175 | .outputOption('-hls_flags single_file') | ||
172 | } | 176 | } |
173 | 177 | ||
174 | command | 178 | command |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 3fc776f1a..5c6dc5e19 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -7,7 +7,7 @@ import { join } from 'path' | |||
7 | 7 | ||
8 | function doRequest <T> ( | 8 | function doRequest <T> ( |
9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
10 | ): Bluebird<{ response: request.RequestResponse, body: any }> { | 10 | ): Bluebird<{ response: request.RequestResponse, body: T }> { |
11 | if (requestOptions.activityPub === true) { | 11 | if (requestOptions.activityPub === true) { |
12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} | 12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} |
13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 3c3406e38..cb0e823c5 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -7,7 +7,6 @@ import { join } from 'path' | |||
7 | import { Instance as ParseTorrent } from 'parse-torrent' | 7 | import { Instance as ParseTorrent } from 'parse-torrent' |
8 | import { remove } from 'fs-extra' | 8 | import { remove } from 'fs-extra' |
9 | import * as memoizee from 'memoizee' | 9 | import * as memoizee from 'memoizee' |
10 | import { isArray } from './custom-validators/misc' | ||
11 | 10 | ||
12 | function deleteFileAsync (path: string) { | 11 | function deleteFileAsync (path: string) { |
13 | remove(path) | 12 | remove(path) |
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 8215840da..a3f379b76 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -355,10 +355,10 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
355 | 355 | ||
356 | logger.info('Fetching remote actor %s.', actorUrl) | 356 | logger.info('Fetching remote actor %s.', actorUrl) |
357 | 357 | ||
358 | const requestResult = await doRequest(options) | 358 | const requestResult = await doRequest<ActivityPubActor>(options) |
359 | normalizeActor(requestResult.body) | 359 | normalizeActor(requestResult.body) |
360 | 360 | ||
361 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON = requestResult.body |
362 | if (isActorObjectValid(actorJSON) === false) { | 362 | if (isActorObjectValid(actorJSON) === false) { |
363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) | 363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
364 | return { result: undefined, statusCode: requestResult.response.statusCode } | 364 | return { result: undefined, statusCode: requestResult.response.statusCode } |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 10db6c3c3..3575981f4 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | import { basename, dirname, join } from 'path' | 2 | import { basename, join, dirname } from 'path' |
3 | import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers' | 3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' |
4 | import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra' | 4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | 5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' |
6 | import { sha256 } from '../helpers/core-utils' | 6 | import { sha256 } from '../helpers/core-utils' |
7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
8 | import HLSDownloader from 'hlsdownloader' | ||
9 | import { logger } from '../helpers/logger' | 8 | import { logger } from '../helpers/logger' |
10 | import { parse } from 'url' | 9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
10 | import { generateRandomString } from '../helpers/utils' | ||
11 | import { flatten, uniq } from 'lodash' | ||
11 | 12 | ||
12 | async function updateMasterHLSPlaylist (video: VideoModel) { | 13 | async function updateMasterHLSPlaylist (video: VideoModel) { |
13 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | 14 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) |
@@ -37,66 +38,119 @@ async function updateMasterHLSPlaylist (video: VideoModel) { | |||
37 | } | 38 | } |
38 | 39 | ||
39 | async function updateSha256Segments (video: VideoModel) { | 40 | async function updateSha256Segments (video: VideoModel) { |
40 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | 41 | const json: { [filename: string]: { [range: string]: string } } = {} |
41 | const files = await readdir(directory) | 42 | |
42 | const json: { [filename: string]: string} = {} | 43 | const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) |
44 | |||
45 | // For all the resolutions available for this video | ||
46 | for (const file of video.VideoFiles) { | ||
47 | const rangeHashes: { [range: string]: string } = {} | ||
48 | |||
49 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | ||
50 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
43 | 51 | ||
44 | for (const file of files) { | 52 | // Maybe the playlist is not generated for this resolution yet |
45 | if (file.endsWith('.ts') === false) continue | 53 | if (!await pathExists(playlistPath)) continue |
46 | 54 | ||
47 | const buffer = await readFile(join(directory, file)) | 55 | const playlistContent = await readFile(playlistPath) |
48 | const filename = basename(file) | 56 | const ranges = getRangesFromPlaylist(playlistContent.toString()) |
49 | 57 | ||
50 | json[filename] = sha256(buffer) | 58 | const fd = await open(videoPath, 'r') |
59 | for (const range of ranges) { | ||
60 | const buf = Buffer.alloc(range.length) | ||
61 | await read(fd, buf, 0, range.length, range.offset) | ||
62 | |||
63 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | ||
64 | } | ||
65 | await close(fd) | ||
66 | |||
67 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | ||
68 | json[videoFilename] = rangeHashes | ||
51 | } | 69 | } |
52 | 70 | ||
53 | const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 71 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) |
54 | await outputJSON(outputPath, json) | 72 | await outputJSON(outputPath, json) |
55 | } | 73 | } |
56 | 74 | ||
57 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { | 75 | function getRangesFromPlaylist (playlistContent: string) { |
58 | let timer | 76 | const ranges: { offset: number, length: number }[] = [] |
77 | const lines = playlistContent.split('\n') | ||
78 | const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ | ||
59 | 79 | ||
60 | logger.info('Importing HLS playlist %s', playlistUrl) | 80 | for (const line of lines) { |
81 | const captured = regex.exec(line) | ||
61 | 82 | ||
62 | const params = { | 83 | if (captured) { |
63 | playlistURL: playlistUrl, | 84 | ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) |
64 | destination: CONFIG.STORAGE.TMP_DIR | 85 | } |
65 | } | 86 | } |
66 | const downloader = new HLSDownloader(params) | ||
67 | |||
68 | const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname)) | ||
69 | 87 | ||
70 | return new Promise<string>(async (res, rej) => { | 88 | return ranges |
71 | downloader.startDownload(err => { | 89 | } |
72 | clearTimeout(timer) | ||
73 | 90 | ||
74 | if (err) { | 91 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { |
75 | deleteTmpDirectory(hlsDestinationDir) | 92 | let timer |
76 | 93 | ||
77 | return rej(err) | 94 | logger.info('Importing HLS playlist %s', playlistUrl) |
78 | } | ||
79 | 95 | ||
80 | move(hlsDestinationDir, destinationDir, { overwrite: true }) | 96 | return new Promise<string>(async (res, rej) => { |
81 | .then(() => res()) | 97 | const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10)) |
82 | .catch(err => { | ||
83 | deleteTmpDirectory(hlsDestinationDir) | ||
84 | 98 | ||
85 | return rej(err) | 99 | await ensureDir(tmpDirectory) |
86 | }) | ||
87 | }) | ||
88 | 100 | ||
89 | timer = setTimeout(() => { | 101 | timer = setTimeout(() => { |
90 | deleteTmpDirectory(hlsDestinationDir) | 102 | deleteTmpDirectory(tmpDirectory) |
91 | 103 | ||
92 | return rej(new Error('HLS download timeout.')) | 104 | return rej(new Error('HLS download timeout.')) |
93 | }, timeout) | 105 | }, timeout) |
94 | 106 | ||
95 | function deleteTmpDirectory (directory: string) { | 107 | try { |
96 | remove(directory) | 108 | // Fetch master playlist |
97 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | 109 | const subPlaylistUrls = await fetchUniqUrls(playlistUrl) |
110 | |||
111 | const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u)) | ||
112 | const fileUrls = uniq(flatten(await Promise.all(subRequests))) | ||
113 | |||
114 | logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls }) | ||
115 | |||
116 | for (const fileUrl of fileUrls) { | ||
117 | const destPath = join(tmpDirectory, basename(fileUrl)) | ||
118 | |||
119 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath) | ||
120 | } | ||
121 | |||
122 | clearTimeout(timer) | ||
123 | |||
124 | await move(tmpDirectory, destinationDir, { overwrite: true }) | ||
125 | |||
126 | return res() | ||
127 | } catch (err) { | ||
128 | deleteTmpDirectory(tmpDirectory) | ||
129 | |||
130 | return rej(err) | ||
98 | } | 131 | } |
99 | }) | 132 | }) |
133 | |||
134 | function deleteTmpDirectory (directory: string) { | ||
135 | remove(directory) | ||
136 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | ||
137 | } | ||
138 | |||
139 | async function fetchUniqUrls (playlistUrl: string) { | ||
140 | const { body } = await doRequest<string>({ uri: playlistUrl }) | ||
141 | |||
142 | if (!body) return [] | ||
143 | |||
144 | const urls = body.split('\n') | ||
145 | .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4')) | ||
146 | .map(url => { | ||
147 | if (url.startsWith('http://') || url.startsWith('https://')) return url | ||
148 | |||
149 | return `${dirname(playlistUrl)}/${url}` | ||
150 | }) | ||
151 | |||
152 | return uniq(urls) | ||
153 | } | ||
100 | } | 154 | } |
101 | 155 | ||
102 | // --------------------------------------------------------------------------- | 156 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 608badfef..086b860a2 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -100,7 +100,10 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti | |||
100 | outputPath, | 100 | outputPath, |
101 | resolution, | 101 | resolution, |
102 | isPortraitMode, | 102 | isPortraitMode, |
103 | generateHlsPlaylist: true | 103 | |
104 | hlsPlaylist: { | ||
105 | videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution) | ||
106 | } | ||
104 | } | 107 | } |
105 | 108 | ||
106 | await transcode(transcodeOptions) | 109 | await transcode(transcodeOptions) |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index bce537781..bf6f7b0c4 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -125,6 +125,10 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
125 | return 'segments-sha256.json' | 125 | return 'segments-sha256.json' |
126 | } | 126 | } |
127 | 127 | ||
128 | static getHlsVideoName (uuid: string, resolution: number) { | ||
129 | return `${uuid}-${resolution}-fragmented.mp4` | ||
130 | } | ||
131 | |||
128 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | 132 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { |
129 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | 133 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) |
130 | } | 134 | } |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 5b99309fb..778611fff 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | viewVideo, | 17 | viewVideo, |
18 | wait, | 18 | wait, |
19 | waitUntilLog, | 19 | waitUntilLog, |
20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer | 20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash |
21 | } from '../../../../shared/utils' | 21 | } from '../../../../shared/utils' |
22 | import { waitJobs } from '../../../../shared/utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | 23 | ||
@@ -178,20 +178,24 @@ async function check1PlaylistRedundancies (videoUUID?: string) { | |||
178 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) | 178 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) |
179 | } | 179 | } |
180 | 180 | ||
181 | await makeGetRequest({ | 181 | const baseUrlPlaylist = servers[1].url + '/static/playlists/hls' |
182 | url: servers[0].url, | 182 | const baseUrlSegment = servers[0].url + '/static/redundancy/hls' |
183 | statusCodeExpected: 200, | 183 | |
184 | path: `/static/redundancy/hls/${videoUUID}/360_000.ts`, | 184 | const res = await getVideo(servers[0].url, videoUUID) |
185 | contentType: null | 185 | const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] |
186 | }) | 186 | |
187 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
188 | await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) | ||
189 | } | ||
187 | 190 | ||
188 | for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) { | 191 | for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) { |
189 | const files = await readdir(join(root(), directory, videoUUID)) | 192 | const files = await readdir(join(root(), directory, videoUUID)) |
190 | expect(files).to.have.length.at.least(4) | 193 | expect(files).to.have.length.at.least(4) |
191 | 194 | ||
192 | for (const resolution of [ 240, 360, 480, 720 ]) { | 195 | for (const resolution of [ 240, 360, 480, 720 ]) { |
193 | expect(files.find(f => f === `${resolution}_000.ts`)).to.not.be.undefined | 196 | const filename = `${videoUUID}-${resolution}-fragmented.mp4` |
194 | expect(files.find(f => f === `${resolution}_001.ts`)).to.not.be.undefined | 197 | |
198 | expect(files.find(f => f === filename)).to.not.be.undefined | ||
195 | } | 199 | } |
196 | } | 200 | } |
197 | } | 201 | } |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts index 71d863b12..a1214bad1 100644 --- a/server/tests/api/videos/video-hls.ts +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -4,13 +4,12 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | checkDirectoryIsEmpty, | 6 | checkDirectoryIsEmpty, |
7 | checkSegmentHash, | ||
7 | checkTmpIsEmpty, | 8 | checkTmpIsEmpty, |
8 | doubleFollow, | 9 | doubleFollow, |
9 | flushAndRunMultipleServers, | 10 | flushAndRunMultipleServers, |
10 | flushTests, | 11 | flushTests, |
11 | getPlaylist, | 12 | getPlaylist, |
12 | getSegment, | ||
13 | getSegmentSha256, | ||
14 | getVideo, | 13 | getVideo, |
15 | killallServers, | 14 | killallServers, |
16 | removeVideo, | 15 | removeVideo, |
@@ -22,7 +21,6 @@ import { | |||
22 | } from '../../../../shared/utils' | 21 | } from '../../../../shared/utils' |
23 | import { VideoDetails } from '../../../../shared/models/videos' | 22 | import { VideoDetails } from '../../../../shared/models/videos' |
24 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | 23 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' |
25 | import { sha256 } from '../../../helpers/core-utils' | ||
26 | import { join } from 'path' | 24 | import { join } from 'path' |
27 | 25 | ||
28 | const expect = chai.expect | 26 | const expect = chai.expect |
@@ -56,19 +54,15 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | |||
56 | const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`) | 54 | const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`) |
57 | 55 | ||
58 | const subPlaylist = res2.text | 56 | const subPlaylist = res2.text |
59 | expect(subPlaylist).to.contain(resolution + '_000.ts') | 57 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) |
60 | } | 58 | } |
61 | } | 59 | } |
62 | 60 | ||
63 | { | 61 | { |
64 | for (const resolution of resolutions) { | 62 | const baseUrl = 'http://localhost:9001/static/playlists/hls' |
65 | |||
66 | const res2 = await getSegment(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}_000.ts`) | ||
67 | 63 | ||
68 | const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url) | 64 | for (const resolution of resolutions) { |
69 | 65 | await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) | |
70 | const sha256Server = resSha.body[ resolution + '_000.ts' ] | ||
71 | expect(sha256(res2.body)).to.equal(sha256Server) | ||
72 | } | 66 | } |
73 | } | 67 | } |
74 | } | 68 | } |