مقدمه
برنامه های افزودنی منبع رسانه (MSE) بافر و کنترل پخش گسترده ای را برای عناصر <audio>
و <video>
HTML5 فراهم می کند. در حالی که در ابتدا برای تسهیل پخش جریانی تطبیقی پویا از طریق پخشکنندههای ویدئویی مبتنی بر HTTP (DASH) توسعه یافته بود، در ادامه خواهیم دید که چگونه میتوان از آنها برای صدا استفاده کرد. مخصوصا برای پخش بدون شکاف
احتمالاً به یک آلبوم موسیقی گوش دادهاید که در آن آهنگها یکپارچه در میان آهنگها جریان داشتند. حتی ممکن است در حال حاضر به یکی از آن ها گوش دهید. هنرمندان این تجربههای پخش بدون شکاف را هم بهعنوان یک انتخاب هنری و هم بهعنوان مصنوع از صفحههای وینیل و سیدی خلق میکنند که در آن صدا به صورت یک جریان پیوسته نوشته میشود. متأسفانه، به دلیل نحوه کار کدک های صوتی مدرن مانند MP3 و AAC ، این تجربه شنیداری یکپارچه اغلب امروزه از بین رفته است.
در زیر به جزئیات دلیل آن خواهیم پرداخت، اما در حال حاضر اجازه دهید با یک نمایش شروع کنیم. در زیر سی ثانیه اول از Sintel عالی است که به پنج فایل MP3 جداگانه خرد شده و با استفاده از MSE دوباره مونتاژ شده است. خطوط قرمز نشان دهنده شکاف های ایجاد شده در هنگام ایجاد (رمزگذاری) هر MP3 است. در این نقاط اشکالاتی را خواهید شنید.
آخه! این یک تجربه عالی نیست. ما می توانیم بهتر انجام دهیم. با کمی کار بیشتر، با استفاده از همان فایل های MP3 در نسخه ی نمایشی بالا، می توانیم از MSE برای حذف آن شکاف های آزار دهنده استفاده کنیم. خطوط سبز در نسخه ی نمایشی بعدی نشان می دهد که فایل ها کجا به هم پیوسته اند و شکاف ها حذف شده اند. در Chrome 38+، این یکپارچه پخش میشود!
راه های مختلفی برای ایجاد محتوای بدون شکاف وجود دارد. برای اهداف این نسخه ی نمایشی، ما بر روی نوع فایل هایی که یک کاربر معمولی ممکن است در اطراف خود داشته باشد تمرکز خواهیم کرد. جایی که هر فایل به طور جداگانه بدون توجه به بخش های صوتی قبل یا بعد از آن رمزگذاری شده است.
راه اندازی اولیه
ابتدا اجازه دهید به عقب برگردیم و تنظیمات اولیه یک نمونه MediaSource
را پوشش دهیم. افزونه های منبع رسانه، همانطور که از نام آن پیداست، فقط پسوند عناصر رسانه ای موجود هستند. در زیر، یک Object URL
که نمایانگر نمونه MediaSource
ما است، به صفت منبع یک عنصر صوتی اختصاص می دهیم. درست مثل یک URL استاندارد.
var audio = document.createElement('audio');
var mediaSource = new MediaSource();
var SEGMENTS = 5;
mediaSource.addEventListener('sourceopen', function () {
var sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
function onAudioLoaded(data, index) {
// Append the ArrayBuffer data into our new SourceBuffer.
sourceBuffer.appendBuffer(data);
}
// Retrieve an audio segment via XHR. For simplicity, we're retrieving the
// entire segment at once, but we could also retrieve it in chunks and append
// each chunk separately. MSE will take care of assembling the pieces.
GET('sintel/sintel_0.mp3', function (data) {
onAudioLoaded(data, 0);
});
});
audio.src = URL.createObjectURL(mediaSource);
هنگامی که شی MediaSource
متصل می شود، مقداری مقداردهی اولیه را انجام می دهد و در نهایت یک رویداد sourceopen
را اجرا می کند. در این مرحله می توانیم یک SourceBuffer
ایجاد کنیم. در مثال بالا، ما یک audio/mpeg
ایجاد میکنیم که میتواند بخشهای MP3 ما را تجزیه و رمزگشایی کند. چندین نوع دیگر موجود است.
شکل موج های غیرعادی
ما در یک لحظه به کد باز خواهیم گشت، اما اجازه دهید اکنون به فایلی که به تازگی ضمیمه کرده ایم، به ویژه در انتهای آن، با دقت بیشتری نگاه کنیم. در زیر، نموداری از آخرین 3000 نمونه میانگین در هر دو کانال از آهنگ sintel_0.mp3
است. هر پیکسل روی خط قرمز یک نمونه ممیز شناور در محدوده [-1.0, 1.0]
است.
آن همه نمونه صفر (بی صدا) چه خبر!؟ آنها در واقع به دلیل آرتیفکت های فشرده سازی هستند که در هنگام رمزگذاری معرفی شده اند. تقریباً هر رمزگذار نوعی از padding را معرفی می کند. در این مورد LAME دقیقاً 576 نمونه padding را به انتهای فایل اضافه کرد.
علاوه بر padding در انتها، هر فایل دارای padding نیز به ابتدا بود. اگر به آهنگ sintel_1.mp3
نگاهی بیندازیم، شاهد 576 نمونه بالشتک دیگر در جلو خواهیم بود. مقدار padding بسته به رمزگذار و محتوا متفاوت است، اما ما مقادیر دقیق را بر اساس metadata
موجود در هر فایل میدانیم.
بخش های سکوت در ابتدا و انتهای هر فایل همان چیزی است که باعث ایجاد اشکال بین بخش ها در نسخه ی نمایشی قبلی می شود. برای دستیابی به پخش بدون شکاف، باید این بخش های سکوت را حذف کنیم. خوشبختانه، این کار به راحتی با MediaSource
انجام می شود. در زیر، روش onAudioLoaded()
خود را تغییر می دهیم تا از یک پنجره ضمیمه و یک افست مهر زمانی برای حذف این سکوت استفاده کنیم.
کد مثال
function onAudioLoaded(data, index) {
// Parsing gapless metadata is unfortunately non trivial and a bit messy, so
// we'll glaze over it here; see the appendix for details.
// ParseGaplessData() will return a dictionary with two elements:
//
// audioDuration: Duration in seconds of all non-padding audio.
// frontPaddingDuration: Duration in seconds of the front padding.
//
var gaplessMetadata = ParseGaplessData(data);
// Each appended segment must be appended relative to the next. To avoid any
// overlaps, we'll use the end timestamp of the last append as the starting
// point for our next append or zero if we haven't appended anything yet.
var appendTime = index > 0 ? sourceBuffer.buffered.end(0) : 0;
// Simply put, an append window allows you to trim off audio (or video) frames
// which fall outside of a specified time range. Here, we'll use the end of
// our last append as the start of our append window and the end of the real
// audio data for this segment as the end of our append window.
sourceBuffer.appendWindowStart = appendTime;
sourceBuffer.appendWindowEnd = appendTime + gaplessMetadata.audioDuration;
// The timestampOffset field essentially tells MediaSource where in the media
// timeline the data given to appendBuffer() should be placed. I.e., if the
// timestampOffset is 1 second, the appended data will start 1 second into
// playback.
//
// MediaSource requires that the media timeline starts from time zero, so we
// need to ensure that the data left after filtering by the append window
// starts at time zero. We'll do this by shifting all of the padding we want
// to discard before our append time (and thus, before our append window).
sourceBuffer.timestampOffset =
appendTime - gaplessMetadata.frontPaddingDuration;
// When appendBuffer() completes, it will fire an updateend event signaling
// that it's okay to append another segment of media. Here, we'll chain the
// append for the next segment to the completion of our current append.
if (index == 0) {
sourceBuffer.addEventListener('updateend', function () {
if (++index < SEGMENTS) {
GET('sintel/sintel_' + index + '.mp3', function (data) {
onAudioLoaded(data, index);
});
} else {
// We've loaded all available segments, so tell MediaSource there are no
// more buffers which will be appended.
mediaSource.endOfStream();
URL.revokeObjectURL(audio.src);
}
});
}
// appendBuffer() will now use the timestamp offset and append window settings
// to filter and timestamp the data we're appending.
//
// Note: While this demo uses very little memory, more complex use cases need
// to be careful about memory usage or garbage collection may remove ranges of
// media in unexpected places.
sourceBuffer.appendBuffer(data);
}
شکل موج بدون درز
بیایید ببینیم کد جدید درخشان ما با نگاهی دوباره به شکل موج پس از اعمال پنجره های ضمیمه خود، چه کاری انجام داده است. در زیر مشاهده می کنید که قسمت بی صدا در انتهای sintel_0.mp3
(قرمز) و قسمت بی صدا در ابتدای sintel_1.mp3
(به رنگ آبی) حذف شده است. ما را با یک انتقال یکپارچه بین بخشها قرار میدهد.
نتیجه گیری
با این کار، ما هر پنج بخش را به طور یکپارچه به یکی دوختیم و متعاقباً به پایان نسخه نمایشی خود رسیدیم. قبل از شروع، ممکن است متوجه شده باشید که متد onAudioLoaded()
ما هیچ توجهی به کانتینرها یا کدک ها ندارد. این بدان معناست که همه این تکنیک ها صرف نظر از نوع کانتینر یا کدک کار خواهند کرد. در زیر می توانید به جای MP3، نسخه ی نمایشی اصلی MP4 تکه تکه شده آماده DASH را دوباره پخش کنید.
اگر میخواهید بیشتر بدانید، پیوستهای زیر را برای نگاه عمیقتر به ایجاد محتوای بدون شکاف و تجزیه فراداده بررسی کنید. همچنین میتوانید gapless.js
برای نگاهی دقیقتر به کدی که این نسخه نمایشی را تامین میکند، کاوش کنید.
با تشکر برای خواندن!
پیوست A: ایجاد محتوای بدون شکاف
ایجاد محتوای بدون شکاف ممکن است به سختی انجام شود. در زیر به ایجاد رسانه Sintel مورد استفاده در این نسخه نمایشی خواهیم پرداخت. برای شروع به یک کپی از موسیقی متن FLAC بدون اتلاف برای Sintel نیاز دارید. برای آیندگان، SHA1 در زیر گنجانده شده است. برای ابزارها، به FFmpeg ، MP4Box ، LAME و نصب OSX با afconvert نیاز دارید.
unzip Jan_Morgenstern-Sintel-FLAC.zip
sha1sum 1-Snow_Fight.flac
# 0535ca207ccba70d538f7324916a3f1a3d550194 1-Snow_Fight.flac
ابتدا، 31.5 ثانیه اول مسیر 1-Snow_Fight.flac
را تقسیم می کنیم. ما همچنین می خواهیم یک محو شدن 2.5 ثانیه ای اضافه کنیم که از 28 ثانیه شروع می شود تا پس از پایان پخش، از هرگونه کلیکی جلوگیری شود. با استفاده از خط فرمان FFmpeg زیر میتوانیم همه اینها را انجام دهیم و نتایج را در sintel.flac
قرار دهیم.
ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac
سپس، فایل را به 5 فایل موجی 6.5 ثانیه ای تقسیم می کنیم. استفاده از موج ساده ترین است زیرا تقریباً هر رمزگذار از جذب آن پشتیبانی می کند. باز هم میتوانیم این کار را دقیقاً با FFmpeg انجام دهیم، پس از آن خواهیم داشت: sintel_0.wav
، sintel_1.wav
، sintel_2.wav
، sintel_3.wav
، و sintel_4.wav
.
ffmpeg -i sintel.flac -acodec pcm_f32le -map 0 -f segment \
-segment_list out.list -segment_time 6.5 sintel_%d.wav
در مرحله بعد، اجازه دهید فایل های MP3 را ایجاد کنیم. LAME چندین گزینه برای ایجاد محتوای بدون شکاف دارد. اگر کنترل محتوا را در دست دارید، میتوانید از --nogap
با کدگذاری دستهای همه فایلها استفاده کنید تا بهکلی از padding بین بخشها جلوگیری کنید. با این حال، برای اهداف این نسخه ی نمایشی، ما آن بالشتک را می خواهیم، بنابراین از یک رمزگذاری استاندارد VBR با کیفیت بالا برای فایل های موج استفاده می کنیم.
lame -V=2 sintel_0.wav sintel_0.mp3
lame -V=2 sintel_1.wav sintel_1.mp3
lame -V=2 sintel_2.wav sintel_2.mp3
lame -V=2 sintel_3.wav sintel_3.mp3
lame -V=2 sintel_4.wav sintel_4.mp3
این تمام چیزی است که برای ایجاد فایل های MP3 لازم است. حال اجازه دهید ایجاد فایل های MP4 تکه تکه شده را پوشش دهیم. ما دستورالعمل های اپل را برای ایجاد رسانه ای که برای iTunes مسلط شده است دنبال می کنیم. در زیر، طبق دستورالعمل، فایلهای موج را به فایلهای CAF میانی تبدیل میکنیم، قبل از اینکه آنها را بهعنوان AAC در یک ظرف MP4 با استفاده از پارامترهای توصیهشده رمزگذاری کنیم.
afconvert sintel_0.wav sintel_0_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_1.wav sintel_1_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_2.wav sintel_2_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_3.wav sintel_3_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_4.wav sintel_4_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_0_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_0.m4a
afconvert sintel_1_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_1.m4a
afconvert sintel_2_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_2.m4a
afconvert sintel_3_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_3.m4a
afconvert sintel_4_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_4.m4a
ما اکنون چندین فایل M4A داریم که باید قبل از استفاده با MediaSource
آنها را به درستی قطعه بندی کنیم . برای اهداف خود، از قطعه قطعه یک ثانیه استفاده خواهیم کرد. MP4Box هر MP4 تکه تکه شده را به عنوان sintel_#_dashinit.mp4
به همراه یک مانیفست MPEG-DASH ( sintel_#_dash.mpd
) می نویسد که می توان آن را دور انداخت.
MP4Box -dash 1000 sintel_0.m4a && mv sintel_0_dashinit.mp4 sintel_0.mp4
MP4Box -dash 1000 sintel_1.m4a && mv sintel_1_dashinit.mp4 sintel_1.mp4
MP4Box -dash 1000 sintel_2.m4a && mv sintel_2_dashinit.mp4 sintel_2.mp4
MP4Box -dash 1000 sintel_3.m4a && mv sintel_3_dashinit.mp4 sintel_3.mp4
MP4Box -dash 1000 sintel_4.m4a && mv sintel_4_dashinit.mp4 sintel_4.mp4
rm sintel_{0,1,2,3,4}_dash.mpd
همین! ما اکنون فایل های MP4 و MP3 تکه تکه شده با ابرداده صحیح لازم برای پخش بدون شکاف داریم. برای جزئیات بیشتر در مورد اینکه این ابرداده چگونه به نظر می رسد، به پیوست B مراجعه کنید.
پیوست B: تجزیه فراداده بدون شکاف
درست مانند ایجاد محتوای بدون شکاف، تجزیه فراداده بدون شکاف می تواند مشکل باشد زیرا هیچ روش استانداردی برای ذخیره سازی وجود ندارد. در زیر به نحوه ذخیره سازی متادیتای بدون شکاف خود توسط دو رمزگذار رایج، LAME و iTunes خواهیم پرداخت. بیایید با تنظیم چند روش کمکی و یک طرح کلی برای ParseGaplessData()
استفاده شده در بالا شروع کنیم.
// Since most MP3 encoders store the gapless metadata in binary, we'll need a
// method for turning bytes into integers. Note: This doesn't work for values
// larger than 2^30 since we'll overflow the signed integer type when shifting.
function ReadInt(buffer) {
var result = buffer.charCodeAt(0);
for (var i = 1; i < buffer.length; ++i) {
result <<= 8;
result += buffer.charCodeAt(i);
}
return result;
}
function ParseGaplessData(arrayBuffer) {
// Gapless data is generally within the first 512 bytes, so limit parsing.
var byteStr = new TextDecoder().decode(arrayBuffer.slice(0, 512));
var frontPadding = 0, endPadding = 0, realSamples = 0;
// ... we'll fill this in as we go below.
ما ابتدا فرمت ابرداده iTunes اپل را پوشش می دهیم زیرا تجزیه و توضیح آن ساده ترین است. در فایلهای MP3 و M4A iTunes (و afconvert) یک بخش کوتاه در ASCII بنویسید:
iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00
این در داخل یک برچسب ID3 در ظرف MP3 و در یک اتم ابرداده در داخل ظرف MP4 نوشته شده است. برای اهداف خود، می توانیم 0000000
توکن اول را نادیده بگیریم. سه نشانه بعدی عبارتند از بالشتک جلویی، بالشتک انتهایی، و تعداد کل نمونه بدون پد. تقسیم هر یک از اینها بر میزان نمونه صدا، مدت زمان هر یک را به ما می دهد.
// iTunes encodes the gapless data as hex strings like so:
//
// 'iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00'
// 'iTunSMPB[ 26 bytes ]####### frontpad endpad real samples'
//
// The approach here elides the complexity of actually parsing MP4 atoms. It
// may not work for all files without some tweaks.
var iTunesDataIndex = byteStr.indexOf('iTunSMPB');
if (iTunesDataIndex != -1) {
var frontPaddingIndex = iTunesDataIndex + 34;
frontPadding = parseInt(byteStr.substr(frontPaddingIndex, 8), 16);
var endPaddingIndex = frontPaddingIndex + 9;
endPadding = parseInt(byteStr.substr(endPaddingIndex, 8), 16);
var sampleCountIndex = endPaddingIndex + 9;
realSamples = parseInt(byteStr.substr(sampleCountIndex, 16), 16);
}
از طرف دیگر، اکثر رمزگذارهای MP3 منبع باز، ابرداده های بدون شکاف را در یک هدر Xing ویژه که در داخل یک قاب MPEG بی صدا قرار داده شده است، ذخیره می کنند (بی صدا است، بنابراین رمزگشاهایی که هدر Xing را درک نمی کنند، به سادگی سکوت را پخش می کنند). متأسفانه این برچسب همیشه وجود ندارد و تعدادی فیلد اختیاری دارد. برای اهداف این نسخه نمایشی، ما کنترل رسانه ها را داریم، اما در عمل برخی بررسی های حساسیت اضافی لازم است تا بدانیم چه زمانی فراداده بدون شکاف واقعاً در دسترس است.
ابتدا تعداد کل نمونه را تجزیه می کنیم. برای سادگی، این را از هدر Xing می خوانیم، اما می توان آن را از هدر صوتی MPEG معمولی ساخت. هدرهای Xing را می توان با تگ Xing
یا Info
مشخص کرد. دقیقاً 4 بایت بعد از این تگ 32 بیتی وجود دارد که تعداد کل فریم های فایل را نشان می دهد. با ضرب این مقدار در تعداد نمونه ها در هر فریم، کل نمونه های موجود در فایل به ما می رسد.
// Xing padding is encoded as 24bits within the header. Note: This code will
// only work for Layer3 Version 1 and Layer2 MP3 files with XING frame counts
// and gapless information. See the following document for more details:
// http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
var xingDataIndex = byteStr.indexOf('Xing');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Info');
if (xingDataIndex != -1) {
// See section 2.3.1 in the link above for the specifics on parsing the Xing
// frame count.
var frameCountIndex = xingDataIndex + 8;
var frameCount = ReadInt(byteStr.substr(frameCountIndex, 4));
// For Layer3 Version 1 and Layer2 there are 1152 samples per frame. See
// section 2.1.5 in the link above for more details.
var paddedSamples = frameCount * 1152;
// ... we'll cover this below.
اکنون که تعداد کل نمونهها را داریم، میتوانیم به خواندن تعداد نمونههای padding برویم. بسته به رمزگذار شما، این ممکن است تحت یک برچسب LAME یا Lavf که در سربرگ Xing تو در تو نوشته شده باشد. دقیقاً 17 بایت بعد از این هدر، 3 بایت وجود دارد که هر کدام به ترتیب نمای جلو و انتهایی را در 12 بیت نشان می دهند.
xingDataIndex = byteStr.indexOf('LAME');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Lavf');
if (xingDataIndex != -1) {
// See http://gabriel.mp3-tech.org/mp3infotag.html#delays for details of
// how this information is encoded and parsed.
var gaplessDataIndex = xingDataIndex + 21;
var gaplessBits = ReadInt(byteStr.substr(gaplessDataIndex, 3));
// Upper 12 bits are the front padding, lower are the end padding.
frontPadding = gaplessBits >> 12;
endPadding = gaplessBits & 0xFFF;
}
realSamples = paddedSamples - (frontPadding + endPadding);
}
return {
audioDuration: realSamples * SECONDS_PER_SAMPLE,
frontPaddingDuration: frontPadding * SECONDS_PER_SAMPLE
};
}
با آن ما یک عملکرد کامل برای تجزیه اکثریت قریب به اتفاق محتوای بدون شکاف داریم. با این حال، موارد لبه قطعاً فراوان هستند، بنابراین قبل از استفاده از کد مشابه در تولید، احتیاط توصیه می شود.
پیوست ج: در مورد جمع آوری زباله
حافظه متعلق به نمونه های SourceBuffer
به طور فعال بر اساس نوع محتوا، محدودیت های خاص پلت فرم و موقعیت پخش فعلی جمع آوری شده است. در Chrome، ابتدا حافظه از بافرهایی که قبلاً پخش شده است، بازیابی می شود. با این حال، اگر میزان استفاده از حافظه از محدودیتهای خاص پلتفرم بیشتر شود، حافظه را از بافرهای پخشنشده حذف میکند.
هنگامی که پخش به دلیل حافظه بازیابی شده به شکافی در خط زمانی برسد، اگر شکاف به اندازه کافی کوچک باشد ممکن است دچار مشکل شود یا اگر شکاف خیلی بزرگ باشد، به طور کامل متوقف شود. هیچ کدام یک تجربه کاربری عالی نیست، بنابراین مهم است که از اضافه کردن داده های بیش از حد به یکباره خودداری کنید و به صورت دستی محدوده هایی را از جدول زمانی رسانه حذف کنید که دیگر ضروری نیستند.
محدوده ها را می توان از طریق متد remove()
در هر SourceBuffer
حذف کرد. که محدوده [start, end]
را در ثانیه می گیرد. مشابه appendBuffer()
، هر remove()
یک رویداد updateend
را پس از اتمام اجرا می کند. حذف یا الحاقات دیگر نباید تا زمانی که رویداد آغاز شود صادر شود.
در کروم دسکتاپ، میتوانید تقریباً 12 مگابایت محتوای صوتی و 150 مگابایت محتوای ویدیویی را همزمان در حافظه نگه دارید. شما نباید در مرورگرها یا پلتفرم ها به این مقادیر تکیه کنید. به عنوان مثال، آنها مطمئناً نماینده دستگاه های تلفن همراه نیستند.
جمع آوری زباله فقط بر داده های اضافه شده به SourceBuffers
تأثیر می گذارد. هیچ محدودیتی برای مقدار داده ای که می توانید در متغیرهای جاوا اسکریپت بافر نگه دارید وجود ندارد. همچنین میتوانید در صورت لزوم همان دادهها را در همان موقعیت اضافه کنید.