Even though the answer from @Matt is the obvious solution for vim, I needed something that will work in neovim as well, so I ended up writing a function that works in both editors.
function! GetVisibleLines()
let l:visible_w = gettabinfo(tabpagenr())[0]['windows']
let l:current_w = win_getid()
let l:visible_l = []
for w in l:visible_w
noautocmd call win_gotoid(w)
call add(
\ l:visible_l,
\ {'win': w,
\ 'buf': getwininfo(w)[0]['bufnr'],
\ 'lines': [line('w0'), line('w$')]})
endfor
noautocmd call win_gotoid(l:current_w)
return l:visible_l
endfunction
It returns a list with an item for each visible window for the current tab containing the window id, the buffer and the visible lines (first and last)
Example
:echo GetVisibleLines()
-----------------------
[
{'buf': 4, 'win': 1015, 'lines': [1, 25]},
{'buf': 4, 'win': 1001, 'lines': [418, 441]},
{'buf': 2, 'win': 1000, 'lines': [1, 12]}
]